Merge branch 'master' into bs/cell-blobpool/ver-1

This commit is contained in:
healthykim 2025-10-24 15:31:58 +09:00
commit c8e5daf85a
227 changed files with 25001 additions and 12652 deletions

View file

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

View file

@ -485,13 +485,13 @@ var bindTests = []struct {
contract Defaulter {
address public caller;
function() {
fallback() external payable {
caller = msg.sender;
}
}
`,
[]string{`6060604052606a8060106000396000f360606040523615601d5760e060020a6000350463fc9c8d3981146040575b605e6000805473ffffffffffffffffffffffffffffffffffffffff191633179055565b606060005473ffffffffffffffffffffffffffffffffffffffff1681565b005b6060908152602090f3`},
[]string{`[{"constant":true,"inputs":[],"name":"caller","outputs":[{"name":"","type":"address"}],"type":"function"}]`},
[]string{`608060405234801561000f575f80fd5b5061013d8061001d5f395ff3fe608060405260043610610021575f3560e01c8063fc9c8d391461006257610022565b5b335f806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055005b34801561006d575f80fd5b5061007661008c565b60405161008391906100ee565b60405180910390f35b5f8054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6100d8826100af565b9050919050565b6100e8816100ce565b82525050565b5f6020820190506101015f8301846100df565b9291505056fea26469706673582212201e9273ecfb1f534644c77f09a25c21baaba81cf1c444ebc071e12a225a23c72964736f6c63430008140033`},
[]string{`[{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"caller","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]`},
`
"math/big"

View file

@ -277,8 +277,10 @@ func (c *BoundContract) RawCreationTransact(opts *TransactOpts, calldata []byte)
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error) {
// todo(rjl493456442) check the payable fallback or receive is defined
// or not, reject invalid transaction at the first place
// Check if payable fallback or receive is defined
if !c.abi.HasReceive() && !(c.abi.HasFallback() && c.abi.Fallback.IsPayable()) {
return nil, fmt.Errorf("contract does not have a payable fallback or receive function")
}
return c.transact(opts, &c.address, nil)
}

View file

@ -100,22 +100,29 @@ func TestWaitDeployed(t *testing.T) {
}
func TestWaitDeployedCornerCases(t *testing.T) {
backend := simulated.NewBackend(
types.GenesisAlloc{
crypto.PubkeyToAddress(testKey.PublicKey): {Balance: big.NewInt(10000000000000000)},
},
var (
backend = simulated.NewBackend(
types.GenesisAlloc{
crypto.PubkeyToAddress(testKey.PublicKey): {Balance: big.NewInt(10000000000000000)},
},
)
head, _ = backend.Client().HeaderByNumber(t.Context(), nil) // Should be child's, good enough
gasPrice = new(big.Int).Add(head.BaseFee, big.NewInt(1))
signer = types.LatestSigner(params.AllDevChainProtocolChanges)
code = common.FromHex("6060604052600a8060106000396000f360606040526008565b00")
ctx, cancel = context.WithCancel(t.Context())
)
defer backend.Close()
head, _ := backend.Client().HeaderByNumber(context.Background(), nil) // Should be child's, good enough
gasPrice := new(big.Int).Add(head.BaseFee, big.NewInt(1))
// Create a transaction to an account.
code := "6060604052600a8060106000396000f360606040526008565b00"
tx := types.NewTransaction(0, common.HexToAddress("0x01"), big.NewInt(0), 3000000, gasPrice, common.FromHex(code))
tx, _ = types.SignTx(tx, types.LatestSigner(params.AllDevChainProtocolChanges), testKey)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 1. WaitDeploy on a transaction that does not deploy a contract, verify it
// returns an error.
tx := types.MustSignNewTx(testKey, signer, &types.LegacyTx{
Nonce: 0,
To: &common.Address{0x01},
Gas: 300000,
GasPrice: gasPrice,
Data: code,
})
if err := backend.Client().SendTransaction(ctx, tx); err != nil {
t.Errorf("failed to send transaction: %q", err)
}
@ -124,14 +131,23 @@ func TestWaitDeployedCornerCases(t *testing.T) {
t.Errorf("error mismatch: want %q, got %q, ", bind.ErrNoAddressInReceipt, err)
}
// Create a transaction that is not mined.
tx = types.NewContractCreation(1, big.NewInt(0), 3000000, gasPrice, common.FromHex(code))
tx, _ = types.SignTx(tx, types.LatestSigner(params.AllDevChainProtocolChanges), testKey)
// 2. Create a contract, but cancel the WaitDeploy before it is mined.
tx = types.MustSignNewTx(testKey, signer, &types.LegacyTx{
Nonce: 1,
Gas: 300000,
GasPrice: gasPrice,
Data: code,
})
// Wait in another thread so that we can quickly cancel it after submitting
// the transaction.
done := make(chan struct{})
go func() {
contextCanceled := errors.New("context canceled")
if _, err := bind.WaitDeployed(ctx, backend.Client(), tx.Hash()); err.Error() != contextCanceled.Error() {
t.Errorf("error mismatch: want %q, got %q, ", contextCanceled, err)
defer close(done)
want := errors.New("context canceled")
_, err := bind.WaitDeployed(ctx, backend.Client(), tx.Hash())
if err == nil || errors.Is(want, err) {
t.Errorf("error mismatch: want %v, got %v", want, err)
}
}()
@ -139,4 +155,11 @@ func TestWaitDeployedCornerCases(t *testing.T) {
t.Errorf("failed to send transaction: %q", err)
}
cancel()
// Wait for goroutine to exit or for a timeout.
select {
case <-done:
case <-time.After(time.Second * 2):
t.Fatalf("failed to cancel wait deploy")
}
}

View file

@ -99,9 +99,10 @@ func (ks *KeyStore) init(keydir string) {
// TODO: In order for this finalizer to work, there must be no references
// to ks. addressCache doesn't keep a reference but unlocked keys do,
// so the finalizer will not trigger until all timed unlocks have expired.
runtime.SetFinalizer(ks, func(m *KeyStore) {
m.cache.close()
})
runtime.AddCleanup(ks, func(c *accountCache) {
c.close()
}, ks.cache)
// Create the initial list of wallets from the cache
accs := ks.cache.accounts()
ks.wallets = make([]accounts.Wallet, len(accs))
@ -195,11 +196,14 @@ func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscripti
// forces a manual refresh (only triggers for systems where the filesystem notifier
// is not running).
func (ks *KeyStore) updater() {
ticker := time.NewTicker(walletRefreshCycle)
defer ticker.Stop()
for {
// Wait for an account update or a refresh timeout
select {
case <-ks.changes:
case <-time.After(walletRefreshCycle):
case <-ticker.C:
}
// Run the wallet refresher
ks.refreshWallets()

View file

@ -2,7 +2,6 @@ clone_depth: 5
version: "{branch}.{build}"
image:
- Ubuntu
- Visual Studio 2019
environment:
@ -17,25 +16,6 @@ install:
- go version
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.
- matrix:
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 nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
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

@ -32,7 +32,7 @@ var (
testServer2 = testServer("testServer2")
testBlock1 = types.NewBeaconBlock(&deneb.BeaconBlock{
Slot: 123,
Slot: 127,
Body: deneb.BeaconBlockBody{
ExecutionPayload: deneb.ExecutionPayload{
BlockNumber: 456,
@ -41,7 +41,7 @@ var (
},
})
testBlock2 = types.NewBeaconBlock(&deneb.BeaconBlock{
Slot: 124,
Slot: 128,
Body: deneb.BeaconBlockBody{
ExecutionPayload: deneb.ExecutionPayload{
BlockNumber: 457,
@ -49,6 +49,14 @@ var (
},
},
})
testFinal1 = types.NewExecutionHeader(&deneb.ExecutionPayloadHeader{
BlockNumber: 395,
BlockHash: zrntcommon.Hash32(common.HexToHash("abbe7625624bf8ddd84723709e2758956289465dd23475f02387e0854942666")),
})
testFinal2 = types.NewExecutionHeader(&deneb.ExecutionPayloadHeader{
BlockNumber: 420,
BlockHash: zrntcommon.Hash32(common.HexToHash("9182a6ef8723654de174283750932ccc092378549836bf4873657eeec474598")),
})
)
type testServer string
@ -66,9 +74,10 @@ func TestBlockSync(t *testing.T) {
ts.AddServer(testServer1, 1)
ts.AddServer(testServer2, 1)
expHeadBlock := func(expHead *types.BeaconBlock) {
expHeadEvent := func(expHead *types.BeaconBlock, expFinal *types.ExecutionHeader) {
t.Helper()
var expNumber, headNumber uint64
var expFinalHash, finalHash common.Hash
if expHead != nil {
p, err := expHead.ExecutionPayload()
if err != nil {
@ -76,19 +85,26 @@ func TestBlockSync(t *testing.T) {
}
expNumber = p.NumberU64()
}
if expFinal != nil {
expFinalHash = expFinal.BlockHash()
}
select {
case event := <-headCh:
headNumber = event.Block.NumberU64()
finalHash = event.Finalized
default:
}
if headNumber != expNumber {
t.Errorf("Wrong head block, expected block number %d, got %d)", expNumber, headNumber)
}
if finalHash != expFinalHash {
t.Errorf("Wrong finalized block, expected block hash %064x, got %064x)", expFinalHash[:], finalHash[:])
}
}
// no block requests expected until head tracker knows about a head
ts.Run(1)
expHeadBlock(nil)
expHeadEvent(nil, nil)
// set block 1 as prefetch head, announced by server 2
head1 := blockHeadInfo(testBlock1)
@ -103,12 +119,13 @@ func TestBlockSync(t *testing.T) {
ts.AddAllowance(testServer2, 1)
ts.Run(3)
// head block still not expected as the fetched block is not the validated head yet
expHeadBlock(nil)
expHeadEvent(nil, nil)
// set as validated head, expect no further requests but block 1 set as head block
ht.validated.Header = testBlock1.Header()
ht.finalized, ht.finalizedPayload = testBlock1.Header(), testFinal1
ts.Run(4)
expHeadBlock(testBlock1)
expHeadEvent(testBlock1, testFinal1)
// set block 2 as prefetch head, announced by server 1
head2 := blockHeadInfo(testBlock2)
@ -126,17 +143,26 @@ func TestBlockSync(t *testing.T) {
// expect req2 retry to server 2
ts.Run(7, testServer2, sync.ReqBeaconBlock(head2.BlockRoot))
// now head block should be unavailable again
expHeadBlock(nil)
expHeadEvent(nil, nil)
// valid response, now head block should be block 2 immediately as it is already validated
// but head event is still not expected because an epoch boundary was crossed and the
// expected finality update has not arrived yet
ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testBlock2)
ts.Run(8)
expHeadBlock(testBlock2)
expHeadEvent(nil, nil)
// expected finality update arrived, now a head event is expected
ht.finalized, ht.finalizedPayload = testBlock2.Header(), testFinal2
ts.Run(9)
expHeadEvent(testBlock2, testFinal2)
}
type testHeadTracker struct {
prefetch types.HeadInfo
validated types.SignedHeader
prefetch types.HeadInfo
validated types.SignedHeader
finalized types.Header
finalizedPayload *types.ExecutionHeader
}
func (h *testHeadTracker) PrefetchHead() types.HeadInfo {
@ -151,13 +177,14 @@ func (h *testHeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) {
}, h.validated.Header != (types.Header{})
}
// TODO add test case for finality
func (h *testHeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
finalized := types.NewExecutionHeader(new(deneb.ExecutionPayloadHeader))
if h.validated.Header == (types.Header{}) || h.finalizedPayload == nil {
return types.FinalityUpdate{}, false
}
return types.FinalityUpdate{
Attested: types.HeaderWithExecProof{Header: h.validated.Header},
Finalized: types.HeaderWithExecProof{PayloadHeader: finalized},
Attested: types.HeaderWithExecProof{Header: h.finalized},
Finalized: types.HeaderWithExecProof{Header: h.finalized, PayloadHeader: h.finalizedPayload},
Signature: h.validated.Signature,
SignatureSlot: h.validated.SignatureSlot,
}, h.validated.Header != (types.Header{})
}, true
}

View file

@ -17,7 +17,7 @@ func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) {
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
BlockValue *hexutil.Big `json:"blockValue" gencodec:"required"`
BlobsBundle *BlobsBundleV1 `json:"blobsBundle"`
BlobsBundle *BlobsBundle `json:"blobsBundle"`
Requests []hexutil.Bytes `json:"executionRequests"`
Override bool `json:"shouldOverrideBuilder"`
Witness *hexutil.Bytes `json:"witness,omitempty"`
@ -42,7 +42,7 @@ func (e *ExecutionPayloadEnvelope) UnmarshalJSON(input []byte) error {
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
BlockValue *hexutil.Big `json:"blockValue" gencodec:"required"`
BlobsBundle *BlobsBundleV1 `json:"blobsBundle"`
BlobsBundle *BlobsBundle `json:"blobsBundle"`
Requests []hexutil.Bytes `json:"executionRequests"`
Override *bool `json:"shouldOverrideBuilder"`
Witness *hexutil.Bytes `json:"witness,omitempty"`

View file

@ -33,8 +33,22 @@ import (
type PayloadVersion byte
var (
// PayloadV1 is the identifier of ExecutionPayloadV1 introduced in paris fork.
// https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#executionpayloadv1
PayloadV1 PayloadVersion = 0x1
// PayloadV2 is the identifier of ExecutionPayloadV2 introduced in shanghai fork.
//
// https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#executionpayloadv2
// ExecutionPayloadV2 has the syntax of ExecutionPayloadV1 and appends a
// single field: withdrawals.
PayloadV2 PayloadVersion = 0x2
// PayloadV3 is the identifier of ExecutionPayloadV3 introduced in cancun fork.
//
// https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#executionpayloadv3
// ExecutionPayloadV3 has the syntax of ExecutionPayloadV2 and appends the new
// fields: blobGasUsed and excessBlobGas.
PayloadV3 PayloadVersion = 0x3
)
@ -106,13 +120,18 @@ type StatelessPayloadStatusV1 struct {
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
BlockValue *big.Int `json:"blockValue" gencodec:"required"`
BlobsBundle *BlobsBundleV1 `json:"blobsBundle"`
BlobsBundle *BlobsBundle `json:"blobsBundle"`
Requests [][]byte `json:"executionRequests"`
Override bool `json:"shouldOverrideBuilder"`
Witness *hexutil.Bytes `json:"witness,omitempty"`
}
type BlobsBundleV1 struct {
// BlobsBundle includes the marshalled sidecar data. Note this structure is
// shared by BlobsBundleV1 and BlobsBundleV2 for the sake of simplicity.
//
// - BlobsBundleV1: proofs contain exactly len(blobs) kzg proofs.
// - BlobsBundleV2: proofs contain exactly CELLS_PER_EXT_BLOB * len(blobs) cell proofs.
type BlobsBundle struct {
Commitments []hexutil.Bytes `json:"commitments"`
Proofs []hexutil.Bytes `json:"proofs"`
Blobs []hexutil.Bytes `json:"blobs"`
@ -125,7 +144,7 @@ type BlobAndProofV1 struct {
type BlobAndProofV2 struct {
Blob hexutil.Bytes `json:"blob"`
CellProofs []hexutil.Bytes `json:"proofs"`
CellProofs []hexutil.Bytes `json:"proofs"` // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs.
}
// JSON type overrides for ExecutionPayloadEnvelope.
@ -327,18 +346,27 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.
}
// Add blobs.
bundle := BlobsBundleV1{
bundle := BlobsBundle{
Commitments: make([]hexutil.Bytes, 0),
Blobs: make([]hexutil.Bytes, 0),
Proofs: make([]hexutil.Bytes, 0),
}
for _, sidecar := range sidecars {
for j := range sidecar.Blobs {
bundle.Blobs = append(bundle.Blobs, hexutil.Bytes(sidecar.Blobs[j][:]))
bundle.Commitments = append(bundle.Commitments, hexutil.Bytes(sidecar.Commitments[j][:]))
bundle.Blobs = append(bundle.Blobs, sidecar.Blobs[j][:])
bundle.Commitments = append(bundle.Commitments, sidecar.Commitments[j][:])
}
// - Before the Osaka fork, only version-0 blob transactions should be packed,
// with the proof length equal to len(blobs).
//
// - After the Osaka fork, only version-1 blob transactions should be packed,
// with the proof length equal to CELLS_PER_EXT_BLOB * len(blobs).
//
// Ideally, length validation should be performed based on the bundle version.
// In practice, this is unnecessary because blob transaction filtering is
// already done during payload construction.
for _, proof := range sidecar.Proofs {
bundle.Proofs = append(bundle.Proofs, hexutil.Bytes(proof[:]))
bundle.Proofs = append(bundle.Proofs, proof[:])
}
}

View file

@ -20,6 +20,7 @@ import (
"crypto/sha256"
"fmt"
"math"
"math/big"
"os"
"slices"
"sort"
@ -90,12 +91,8 @@ func (c *ChainConfig) AddFork(name string, epoch uint64, version []byte) *ChainC
// LoadForks parses the beacon chain configuration file (config.yaml) and extracts
// the list of forks.
func (c *ChainConfig) LoadForks(path string) error {
file, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read beacon chain config file: %v", err)
}
config := make(map[string]string)
func (c *ChainConfig) LoadForks(file []byte) error {
config := make(map[string]any)
if err := yaml.Unmarshal(file, &config); err != nil {
return fmt.Errorf("failed to parse beacon chain config file: %v", err)
}
@ -108,18 +105,36 @@ func (c *ChainConfig) LoadForks(path string) error {
for key, value := range config {
if strings.HasSuffix(key, "_FORK_VERSION") {
name := key[:len(key)-len("_FORK_VERSION")]
if v, err := hexutil.Decode(value); err == nil {
switch version := value.(type) {
case int:
versions[name] = new(big.Int).SetUint64(uint64(version)).FillBytes(make([]byte, 4))
case uint64:
versions[name] = new(big.Int).SetUint64(version).FillBytes(make([]byte, 4))
case string:
v, err := hexutil.Decode(version)
if err != nil {
return fmt.Errorf("failed to decode hex fork id %q in beacon chain config file: %v", version, err)
}
versions[name] = v
} else {
return fmt.Errorf("failed to decode hex fork id %q in beacon chain config file: %v", value, err)
default:
return fmt.Errorf("invalid fork version %q in beacon chain config file", version)
}
}
if strings.HasSuffix(key, "_FORK_EPOCH") {
name := key[:len(key)-len("_FORK_EPOCH")]
if v, err := strconv.ParseUint(value, 10, 64); err == nil {
switch epoch := value.(type) {
case int:
epochs[name] = uint64(epoch)
case uint64:
epochs[name] = epoch
case string:
v, err := strconv.ParseUint(epoch, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse epoch number %q in beacon chain config file: %v", epoch, err)
}
epochs[name] = v
} else {
return fmt.Errorf("failed to parse epoch number %q in beacon chain config file: %v", value, err)
default:
return fmt.Errorf("invalid fork epoch %q in beacon chain config file", epoch)
}
}
}

View file

@ -0,0 +1,34 @@
package params
import (
"bytes"
"testing"
)
func TestChainConfig_LoadForks(t *testing.T) {
const config = `
GENESIS_FORK_VERSION: 0x00000000
ALTAIR_FORK_VERSION: 0x00000001
ALTAIR_FORK_EPOCH: 1
EIP7928_FORK_VERSION: 0xb0000038
EIP7928_FORK_EPOCH: 18446744073709551615
BLOB_SCHEDULE: []
`
c := &ChainConfig{}
err := c.LoadForks([]byte(config))
if err != nil {
t.Fatal(err)
}
for _, fork := range c.Forks {
if fork.Name == "GENESIS" && (fork.Epoch != 0) {
t.Errorf("unexpected genesis fork epoch %d", fork.Epoch)
}
if fork.Name == "ALTAIR" && (fork.Epoch != 1 || !bytes.Equal(fork.Version, []byte{0, 0, 0, 1})) {
t.Errorf("unexpected altair fork epoch %d version %x", fork.Epoch, fork.Version)
}
}
}

View file

@ -1,9 +1,9 @@
# This file contains sha256 checksums of optional build dependencies.
# version:spec-tests fusaka-devnet-3%40v1.0.0
# version:spec-tests v5.1.0
# https://github.com/ethereum/execution-spec-tests/releases
# https://github.com/ethereum/execution-spec-tests/releases/download/fusaka-devnet-3%40v1.0.0
576261e1280e5300c458aa9b05eccb2fec5ff80a0005940dc52fa03fdd907249 fixtures_fusaka-devnet-3.tar.gz
# https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0
a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz
# version:golang 1.25.1
# https://go.dev/dl/

View file

@ -57,6 +57,7 @@ import (
"time"
"github.com/cespare/cp"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/signify"
"github.com/ethereum/go-ethereum/internal/build"
"github.com/ethereum/go-ethereum/internal/download"
@ -64,6 +65,11 @@ import (
)
var (
goModules = []string{
".",
"./cmd/keeper",
}
// Files that end up in the geth*.zip archive.
gethArchiveFiles = []string{
"COPYING",
@ -143,7 +149,7 @@ func executablePath(name string) string {
func main() {
log.SetFlags(log.Lshortfile)
if !build.FileExist(filepath.Join("build", "ci.go")) {
if !common.FileExist(filepath.Join("build", "ci.go")) {
log.Fatal("this script must be run from the root of the repository")
}
if len(os.Args) < 2 {
@ -216,7 +222,7 @@ func doInstall(cmdline []string) {
// Default: collect all 'main' packages in cmd/ and build those.
packages := flag.Args()
if len(packages) == 0 {
packages = build.FindMainPackages("./cmd")
packages = build.FindMainPackages(&tc, "./cmd/...")
}
// Do the build!
@ -275,26 +281,33 @@ func buildFlags(env build.Environment, staticLinking bool, buildTags []string) (
func doTest(cmdline []string) {
var (
dlgo = flag.Bool("dlgo", false, "Download Go and build with it")
arch = flag.String("arch", "", "Run tests for given architecture")
cc = flag.String("cc", "", "Sets C compiler binary")
coverage = flag.Bool("coverage", false, "Whether to record code coverage")
verbose = flag.Bool("v", false, "Whether to log verbosely")
race = flag.Bool("race", false, "Execute the race detector")
short = flag.Bool("short", false, "Pass the 'short'-flag to go test")
cachedir = flag.String("cachedir", "./build/cache", "directory for caching downloads")
dlgo = flag.Bool("dlgo", false, "Download Go and build with it")
arch = flag.String("arch", "", "Run tests for given architecture")
cc = flag.String("cc", "", "Sets C compiler binary")
coverage = flag.Bool("coverage", false, "Whether to record code coverage")
verbose = flag.Bool("v", false, "Whether to log verbosely")
race = flag.Bool("race", false, "Execute the race detector")
short = flag.Bool("short", false, "Pass the 'short'-flag to go test")
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)
// Get test fixtures.
// Load checksums file (needed for both spec tests and dlgo)
csdb := download.MustLoadChecksums("build/checksums.txt")
downloadSpecTestFixtures(csdb, *cachedir)
// Get test fixtures.
if !*skipspectests {
downloadSpecTestFixtures(csdb, *cachedir)
}
// Configure the toolchain.
tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
if *dlgo {
tc.Root = build.DownloadGo(csdb)
}
gotest := tc.Go("test")
// CI needs a bit more time for the statetests (default 45m).
@ -308,7 +321,7 @@ func doTest(cmdline []string) {
// Test a single package at a time. CI builders are slow
// 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 {
gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover")
}
@ -323,17 +336,25 @@ func doTest(cmdline []string) {
}
packages := flag.CommandLine.Args()
if len(packages) == 0 {
packages = workspacePackagePatterns()
if len(packages) > 0 {
gotest.Args = append(gotest.Args, packages...)
build.MustRun(gotest)
return
}
// No packages specified, run all tests for all modules.
gotest.Args = append(gotest.Args, "./...")
for _, mod := range goModules {
test := *gotest
test.Dir = mod
build.MustRun(&test)
}
gotest.Args = append(gotest.Args, packages...)
build.MustRun(gotest)
}
// downloadSpecTestFixtures downloads and extracts the execution-spec-tests fixtures.
func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string {
ext := ".tar.gz"
base := "fixtures_fusaka-devnet-3"
base := "fixtures_develop"
archivePath := filepath.Join(cachedir, base+ext)
if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil {
log.Fatal(err)
@ -351,40 +372,46 @@ func doCheckGenerate() {
cachedir = flag.String("cachedir", "./build/cache", "directory for caching binaries.")
tc = new(build.GoToolchain)
)
// Compute the origin hashes of all the files
var hashes map[string][32]byte
var err error
hashes, err = build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"})
if err != nil {
log.Fatal("Error computing hashes", "err", err)
}
// Run any go generate steps we might be missing
var (
protocPath = downloadProtoc(*cachedir)
protocGenGoPath = downloadProtocGenGo(*cachedir)
)
c := tc.Go("generate", workspacePackagePatterns()...)
pathList := []string{filepath.Join(protocPath, "bin"), protocGenGoPath, os.Getenv("PATH")}
c.Env = append(c.Env, "PATH="+strings.Join(pathList, string(os.PathListSeparator)))
build.MustRun(c)
// Check if generate file hashes have changed
generated, err := build.HashFolder(".", []string{"tests/testdata", "build/cache", ".git"})
if err != nil {
log.Fatalf("Error re-computing hashes: %v", err)
}
updates := build.DiffHashes(hashes, generated)
for _, file := range updates {
log.Printf("File changed: %s", file)
}
if len(updates) != 0 {
log.Fatal("One or more generated files were updated by running 'go generate ./...'")
for _, mod := range goModules {
// Compute the origin hashes of all the files
hashes, err := build.HashFolder(mod, []string{"tests/testdata", "build/cache", ".git"})
if err != nil {
log.Fatal("Error computing hashes", "err", err)
}
c := tc.Go("generate", "./...")
c.Env = append(c.Env, "PATH="+strings.Join(pathList, string(os.PathListSeparator)))
c.Dir = mod
build.MustRun(c)
// Check if generate file hashes have changed
generated, err := build.HashFolder(mod, []string{"tests/testdata", "build/cache", ".git"})
if err != nil {
log.Fatalf("Error re-computing hashes: %v", err)
}
updates := build.DiffHashes(hashes, generated)
for _, file := range updates {
log.Printf("File changed: %s", file)
}
if len(updates) != 0 {
log.Fatal("One or more generated files were updated by running 'go generate ./...'")
}
}
fmt.Println("No stale files detected.")
// Run go mod tidy check.
build.MustRun(tc.Go("mod", "tidy", "-diff"))
for _, mod := range goModules {
tidy := tc.Go("mod", "tidy", "-diff")
tidy.Dir = mod
build.MustRun(tidy)
}
fmt.Println("No untidy module files detected.")
}
@ -425,20 +452,29 @@ func doLint(cmdline []string) {
)
flag.CommandLine.Parse(cmdline)
packages := flag.CommandLine.Args()
if len(packages) == 0 {
// Get module directories in workspace.
packages = []string{"./..."}
modules := workspaceModules()
for _, m := range modules[1:] {
dir := strings.TrimPrefix(m, modules[0])
packages = append(packages, "."+dir+"/...")
}
linter := downloadLinter(*cachedir)
linter, err := filepath.Abs(linter)
if err != nil {
log.Fatal(err)
}
config, err := filepath.Abs(".golangci.yml")
if err != nil {
log.Fatal(err)
}
linter := downloadLinter(*cachedir)
lflags := []string{"run", "--config", ".golangci.yml"}
build.MustRunCommandWithOutput(linter, append(lflags, packages...)...)
lflags := []string{"run", "--config", config}
packages := flag.CommandLine.Args()
if len(packages) > 0 {
build.MustRunCommandWithOutput(linter, append(lflags, packages...)...)
} else {
// Run for all modules in workspace.
for _, mod := range goModules {
args := append(lflags, "./...")
lintcmd := exec.Command(linter, args...)
lintcmd.Dir = mod
build.MustRunWithOutput(lintcmd)
}
}
fmt.Println("You have achieved perfection.")
}
@ -866,7 +902,7 @@ func ppaUpload(workdir, ppa, sshUser string, files []string) {
var idfile string
if sshkey := getenvBase64("PPA_SSH_KEY"); len(sshkey) > 0 {
idfile = filepath.Join(workdir, "sshkey")
if !build.FileExist(idfile) {
if !common.FileExist(idfile) {
os.WriteFile(idfile, sshkey, 0600)
}
}
@ -1176,31 +1212,3 @@ func doSanityCheck() {
csdb := download.MustLoadChecksums("build/checksums.txt")
csdb.DownloadAndVerifyAll()
}
// workspaceModules lists the module paths in the current work.
func workspaceModules() []string {
listing, err := new(build.GoToolchain).Go("list", "-m").Output()
if err != nil {
log.Fatalf("go list failed:", err)
}
var modules []string
for _, m := range bytes.Split(listing, []byte("\n")) {
m = bytes.TrimSpace(m)
if len(m) > 0 {
modules = append(modules, string(m))
}
}
if len(modules) == 0 {
panic("no modules found")
}
return modules
}
func workspacePackagePatterns() []string {
modules := workspaceModules()
patterns := make([]string, len(modules))
for i, m := range modules {
patterns[i] = m + "/..."
}
return patterns
}

View file

@ -121,7 +121,7 @@ with our test chain. The chain files are located in `./cmd/devp2p/internal/ethte
--nat=none \
--networkid 3503995874084926 \
--verbosity 5 \
--authrpc.jwtsecret 0x7365637265747365637265747365637265747365637265747365637265747365
--authrpc.jwtsecret jwt.secret
Note that the tests also require access to the engine API.
The test suite can now be executed using the devp2p tool.
@ -130,7 +130,7 @@ The test suite can now be executed using the devp2p tool.
--chain internal/ethtest/testdata \
--node enode://.... \
--engineapi http://127.0.0.1:8551 \
--jwtsecret 0x7365637265747365637265747365637265747365637265747365637265747365
--jwtsecret $(cat jwt.secret)
Repeat the above process (re-initialising the node) in order to run the Eth Protocol test suite again.

View file

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

View file

@ -86,3 +86,9 @@ func protoOffset(proto Proto) uint64 {
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,
startingHash: zero,
limitHash: ffHash,
expAccounts: 86,
expAccounts: 67,
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.",
},
{
@ -96,9 +96,9 @@ func (s *Suite) TestSnapGetAccountRange(t *utesting.T) {
root: root,
startingHash: zero,
limitHash: ffHash,
expAccounts: 65,
expAccounts: 49,
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.",
},
{
@ -106,9 +106,9 @@ func (s *Suite) TestSnapGetAccountRange(t *utesting.T) {
root: root,
startingHash: zero,
limitHash: ffHash,
expAccounts: 44,
expAccounts: 34,
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.",
},
{
@ -177,9 +177,9 @@ The server should return the first available account.`,
root: root,
startingHash: firstKey,
limitHash: ffHash,
expAccounts: 86,
expAccounts: 67,
expFirst: firstKey,
expLast: common.HexToHash("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099"),
expLast: common.HexToHash("0x622e662246601dd04f996289ce8b85e86db7bb15bb17f86487ec9d543ddb6f9a"),
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.`,
},
@ -188,9 +188,9 @@ The server should return the first available account of the state as the first i
root: root,
startingHash: hashAdd(firstKey, 1),
limitHash: ffHash,
expAccounts: 86,
expAccounts: 67,
expFirst: secondKey,
expLast: common.HexToHash("0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa"),
expLast: common.HexToHash("0x66192e4c757fba1cdc776e6737008f42d50370d3cd801db3624274283bf7cd63"),
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.`,
},
@ -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),
startingHash: zero,
limitHash: ffHash,
expAccounts: 84,
expAccounts: 66,
expFirst: firstKey,
expLast: common.HexToHash("0x580aa878e2f92d113a12c0a3ce3c21972b03dbe80786858d49a72097e2c491a3"),
expLast: common.HexToHash("0x729953a43ed6c913df957172680a17e5735143ad767bda8f58ac84ec62fbec5e"),
desc: `This test requests data at a state root that is 127 blocks old.
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
// 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.
common.HexToHash("0x3e963a69401a70224cbfb8c0cc2249b019041a538675d71ccf80c9328d114e2e"),
common.HexToHash("0xd0670d09cdfbf3c6320eb3e92c47c57baa6c226551a2d488c05581091e6b1689"),
common.HexToHash("0x5bdc0d6057b35642a16d27223ea5454e5a17a400e28f7328971a5f2a87773b76"),
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,
@ -678,8 +678,8 @@ The server should reject the request.`,
// be updated when the test chain is changed.
expHashes: []common.Hash{
empty,
common.HexToHash("0xd0670d09cdfbf3c6320eb3e92c47c57baa6c226551a2d488c05581091e6b1689"),
common.HexToHash("0x3e963a69401a70224cbfb8c0cc2249b019041a538675d71ccf80c9328d114e2e"),
common.HexToHash("0x0a76c9812ca90ffed8ee4d191e683f93386b6e50cfe3679c0760d27510aa7fc5"),
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) {
t.Log(`This test requests blocks headers from the node, performing two requests
concurrently, with different request IDs.`)
conn, err := s.dialAndPeer(nil)
if err != nil {
t.Fatalf("peering failed: %v", err)
@ -235,37 +236,36 @@ concurrently, with different request IDs.`)
}
// Wait for responses.
headers1 := new(eth.BlockHeadersPacket)
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers1); err != nil {
t.Fatalf("error reading block headers msg: %v", err)
}
if got, want := headers1.RequestId, req1.RequestId; got != want {
t.Fatalf("unexpected request id in response: got %d, want %d", got, want)
}
headers2 := new(eth.BlockHeadersPacket)
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)
// Note they can arrive in either order.
resp, err := collectResponses(conn, 2, func(msg *eth.BlockHeadersPacket) uint64 {
if msg.RequestId != 111 && msg.RequestId != 222 {
t.Fatalf("response with unknown request ID: %v", msg.RequestId)
}
return msg.RequestId
})
if err != nil {
t.Fatal(err)
}
// Check received headers for accuracy.
// Check if headers match.
resp1 := resp[111]
if expected, err := s.chain.GetHeaders(req1); err != nil {
t.Fatalf("failed to get expected headers for request 1: %v", err)
} else if !headersMatch(expected, headers1.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers1)
} else if !headersMatch(expected, resp1.BlockHeadersRequest) {
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 {
t.Fatalf("failed to get expected headers for request 2: %v", err)
} else if !headersMatch(expected, headers2.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers2)
} else if !headersMatch(expected, resp2.BlockHeadersRequest) {
t.Fatalf("header mismatch for request ID %v: \nexpected %v \ngot %v", 222, expected, resp2)
}
}
func (s *Suite) TestSameRequestID(t *utesting.T) {
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.`)
conn, err := s.dialAndPeer(nil)
if err != nil {
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{
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)
}
// Wait for the responses.
headers1 := new(eth.BlockHeadersPacket)
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers1); err != nil {
t.Fatalf("error reading from connection: %v", err)
}
if got, want := headers1.RequestId, request1.RequestId; got != want {
t.Fatalf("unexpected request id: got %d, want %d", got, want)
}
headers2 := new(eth.BlockHeadersPacket)
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)
// Wait for the responses. They can arrive in either order, and we can't tell them
// apart by their request ID, so use the number of headers instead.
resp, err := collectResponses(conn, 2, func(msg *eth.BlockHeadersPacket) uint64 {
id := uint64(len(msg.BlockHeadersRequest))
if id != 2 && id != 3 {
t.Fatalf("invalid number of headers in response: %d", id)
}
return id
})
if err != nil {
t.Fatal(err)
}
// Check if headers match.
resp1 := resp[2]
if expected, err := s.chain.GetHeaders(request1); err != nil {
t.Fatalf("failed to get expected block headers: %v", err)
} else if !headersMatch(expected, headers1.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers1)
t.Fatalf("failed to get expected headers for request 1: %v", err)
} else if !headersMatch(expected, resp1.BlockHeadersRequest) {
t.Fatalf("headers mismatch: \nexpected %v \ngot %v", expected, resp1)
}
resp2 := resp[3]
if expected, err := s.chain.GetHeaders(request2); err != nil {
t.Fatalf("failed to get expected block headers: %v", err)
} else if !headersMatch(expected, headers2.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers2)
t.Fatalf("failed to get expected headers for request 2: %v", err)
} else if !headersMatch(expected, resp2.BlockHeadersRequest) {
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) {
t.Log(`This test sends a GetBlockHeaders message with a request-id of zero,
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)
for i := 0; i < count; i++ {
// Make blob data, max of 2 blobs per tx.
blobdata := make([]byte, blobs%3)
blobdata := make([]byte, min(blobs, 2))
for i := range blobdata {
blobdata[i] = discriminator
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_FORK_ARROW_GLACIER": "60",
"HIVE_FORK_BERLIN": "48",
"HIVE_FORK_BYZANTIUM": "18",
"HIVE_FORK_CONSTANTINOPLE": "24",
"HIVE_FORK_GRAY_GLACIER": "66",
"HIVE_FORK_ARROW_GLACIER": "0",
"HIVE_FORK_BERLIN": "0",
"HIVE_FORK_BYZANTIUM": "0",
"HIVE_FORK_CONSTANTINOPLE": "0",
"HIVE_FORK_GRAY_GLACIER": "0",
"HIVE_FORK_HOMESTEAD": "0",
"HIVE_FORK_ISTANBUL": "36",
"HIVE_FORK_LONDON": "54",
"HIVE_FORK_MUIR_GLACIER": "42",
"HIVE_FORK_PETERSBURG": "30",
"HIVE_FORK_SPURIOUS": "12",
"HIVE_FORK_TANGERINE": "6",
"HIVE_MERGE_BLOCK_ID": "72",
"HIVE_FORK_ISTANBUL": "0",
"HIVE_FORK_LONDON": "0",
"HIVE_FORK_MUIR_GLACIER": "0",
"HIVE_FORK_PETERSBURG": "0",
"HIVE_FORK_SPURIOUS": "0",
"HIVE_FORK_TANGERINE": "0",
"HIVE_MERGE_BLOCK_ID": "0",
"HIVE_NETWORK_ID": "3503995874084926",
"HIVE_SHANGHAI_TIMESTAMP": "780",
"HIVE_TERMINAL_TOTAL_DIFFICULTY": "9454784"
"HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION": "5007716",
"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": {
"chainId": 3503995874084926,
"homesteadBlock": 0,
"eip150Block": 6,
"eip155Block": 12,
"eip158Block": 12,
"byzantiumBlock": 18,
"constantinopleBlock": 24,
"petersburgBlock": 30,
"istanbulBlock": 36,
"muirGlacierBlock": 42,
"berlinBlock": 48,
"londonBlock": 54,
"arrowGlacierBlock": 60,
"grayGlacierBlock": 66,
"mergeNetsplitBlock": 72,
"shanghaiTime": 780,
"cancunTime": 840,
"terminalTotalDifficulty": 9454784,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"arrowGlacierBlock": 0,
"grayGlacierBlock": 0,
"mergeNetsplitBlock": 0,
"shanghaiTime": 0,
"cancunTime": 60,
"pragueTime": 120,
"terminalTotalDifficulty": 131072,
"depositContractAddress": "0x0000000000000000000000000000000000000000",
"ethash": {},
"blobSchedule": {
"cancun": {
"target": 3,
"max": 6,
"baseFeeUpdateFraction": 3338477
},
"prague": {
"target": 6,
"max": 9,
"baseFeeUpdateFraction": 5007716
}
}
},
@ -35,6 +42,18 @@
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"00000961ef480eb55e80d19ad83579a64c007002": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd",
"balance": "0x1"
},
"0000bbddc7ce488642fb579f8b00f3a590007251": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd",
"balance": "0x1"
},
"0000f90827f1c53a10cb7a02335b175320002935": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500",
"balance": "0x1"
},
"000f3df6d732807ef1319fb7b8bb8522d0beac02": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500",
"balance": "0x2a"
@ -81,6 +100,10 @@
"7435ed30a8b4aeb0877cef0c6e8cffe834eb865f": {
"balance": "0xc097ce7bc90715b34b9f1000000000"
},
"7dcd17433742f4c0ca53122ab541d0ba67fc27df": {
"code": "0x3680600080376000206000548082558060010160005560005263656d697460206000a2",
"balance": "0x0"
},
"83c7e323d189f18725ac510004fdc2941f8c4a78": {
"balance": "0xc097ce7bc90715b34b9f1000000000"
},
@ -112,7 +135,7 @@
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas": null,
"baseFeePerGas": "0x3b9aca00",
"excessBlobGas": null,
"blobGasUsed": null
}
}

View file

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

View file

@ -1,12 +1,12 @@
{
"jsonrpc": "2.0",
"id": "fcu500",
"id": "fcu600",
"method": "engine_forkchoiceUpdatedV3",
"params": [
{
"headBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7",
"safeBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7",
"finalizedBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7"
"headBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0",
"safeBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0",
"finalizedBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0"
},
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

@ -143,9 +143,6 @@ type testParams struct {
func cliTestParams(ctx *cli.Context) *testParams {
nodeStr := ctx.String(testNodeFlag.Name)
if nodeStr == "" {
exit(fmt.Errorf("missing -%s", testNodeFlag.Name))
}
node, err := parseNode(nodeStr)
if err != nil {
exit(err)
@ -156,14 +153,5 @@ func cliTestParams(ctx *cli.Context) *testParams {
jwt: ctx.String(testNodeJWTFlag.Name),
chainDir: ctx.String(testChainDirFlag.Name),
}
if p.engineAPI == "" {
exit(fmt.Errorf("missing -%s", testNodeEngineFlag.Name))
}
if p.jwt == "" {
exit(fmt.Errorf("missing -%s", testNodeJWTFlag.Name))
}
if p.chainDir == "" {
exit(fmt.Errorf("missing -%s", testChainDirFlag.Name))
}
return &p
}

View file

@ -39,26 +39,29 @@ var (
}
// for eth/snap tests
testChainDirFlag = &cli.StringFlag{
testChainDirFlag = &cli.PathFlag{
Name: "chain",
Usage: "Test chain directory (required)",
Category: flags.TestingCategory,
Required: true,
}
testNodeFlag = &cli.StringFlag{
Name: "node",
Usage: "Peer-to-Peer endpoint (ENR) of the test node (required)",
Category: flags.TestingCategory,
Required: true,
}
testNodeJWTFlag = &cli.StringFlag{
Name: "jwtsecret",
Usage: "JWT secret for the engine API of the test node (required)",
Category: flags.TestingCategory,
Value: "0x7365637265747365637265747365637265747365637265747365637265747365",
Required: true,
}
testNodeEngineFlag = &cli.StringFlag{
Name: "engineapi",
Usage: "Engine API endpoint of the test node (required)",
Category: flags.TestingCategory,
Required: true,
}
// These two are specific to the discovery tests.

View file

@ -274,10 +274,10 @@ func checkAccumulator(e *era.Era) error {
for it.Next() {
// 1) next() walks the block index, so we're able to implicitly verify it.
if it.Error() != nil {
return fmt.Errorf("error reading block %d: %w", it.Number(), err)
return fmt.Errorf("error reading block %d: %w", it.Number(), it.Error())
}
block, receipts, err := it.BlockAndReceipts()
if it.Error() != nil {
if err != nil {
return fmt.Errorf("error reading block %d: %w", it.Number(), err)
}
// 2) recompute tx root and verify against header.
@ -294,6 +294,9 @@ func checkAccumulator(e *era.Era) error {
td.Add(td, block.Difficulty())
tds = append(tds, new(big.Int).Set(td))
}
if it.Error() != nil {
return fmt.Errorf("error reading block %d: %w", it.Number(), it.Error())
}
// 4+5) Verify accumulator and total difficulty.
got, err := era.ComputeAccumulator(hashes, tds)
if err != nil {

View file

@ -191,6 +191,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
Time: pre.Env.ParentTimestamp,
ExcessBlobGas: pre.Env.ParentExcessBlobGas,
BlobGasUsed: pre.Env.ParentBlobGasUsed,
BaseFee: pre.Env.ParentBaseFee,
}
header := &types.Header{
Time: pre.Env.Timestamp,
@ -373,7 +374,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
func MakePreState(db ethdb.Database, accounts types.GenesisAlloc) *state.StateDB {
tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true})
sdb := state.NewDatabase(tdb, nil)
statedb, _ := state.New(types.EmptyRootHash, sdb)
statedb, err := state.New(types.EmptyRootHash, sdb)
if err != nil {
panic(fmt.Errorf("failed to create initial state: %v", err))
}
for addr, a := range accounts {
statedb.SetCode(addr, a.Code, tracing.CodeChangeUnspecified)
statedb.SetNonce(addr, a.Nonce, tracing.NonceChangeGenesis)
@ -383,8 +387,14 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc) *state.StateDB
}
}
// Commit and re-open to start with a clean state.
root, _ := statedb.Commit(0, false, false)
statedb, _ = state.New(root, sdb)
root, err := statedb.Commit(0, false, false)
if err != nil {
panic(fmt.Errorf("failed to commit initial state: %v", err))
}
statedb, err = state.New(root, sdb)
if err != nil {
panic(fmt.Errorf("failed to reopen state after commit: %v", err))
}
return statedb
}

View file

@ -296,6 +296,14 @@ func TestT8n(t *testing.T) {
output: t8nOutput{alloc: true, result: true},
expOut: "exp.json",
},
{ // Osaka test, EIP-7918 blob gas with parent base fee
base: "./testdata/34",
input: t8nInput{
"alloc.json", "txs.json", "env.json", "Osaka", "",
},
output: t8nOutput{alloc: true, result: true},
expOut: "exp.json",
},
} {
args := []string{"t8n"}
args = append(args, tc.output.get()...)

6
cmd/evm/testdata/34/README.md vendored Normal file
View file

@ -0,0 +1,6 @@
This test verifies that Osaka fork blob gas calculation works correctly when
parentBaseFee is provided. It tests the EIP-7918 reserve price calculation
which requires parent.BaseFee to be properly set.
Regression test for: nil pointer dereference when parent.BaseFee was not
included in the parent header during Osaka fork blob gas calculations.

6
cmd/evm/testdata/34/alloc.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
"balance": "0x1000000000000000000",
"nonce": "0x0"
}
}

18
cmd/evm/testdata/34/env.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"currentCoinbase": "0x0000000000000000000000000000000000000000",
"currentDifficulty": "0x0",
"currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000000",
"currentGasLimit": "0x5f5e100",
"currentNumber": "0x1",
"currentTimestamp": "0x1000",
"parentTimestamp": "0x0",
"currentBaseFee": "0x10",
"parentBaseFee": "0x0a",
"parentGasUsed": "0x0",
"parentGasLimit": "0x5f5e100",
"currentExcessBlobGas": "0x0",
"parentExcessBlobGas": "0x0",
"parentBlobGasUsed": "0x20000",
"parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
"withdrawals": []
}

23
cmd/evm/testdata/34/exp.json vendored Normal file
View file

@ -0,0 +1,23 @@
{
"alloc": {
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {
"balance": "0x1000000000000000000"
}
},
"result": {
"stateRoot": "0x01c28492482a1a1f66224726ef1059a7036fce69d1d2c991b65cd013725d5742",
"txRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"logsHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"currentDifficulty": null,
"receipts": [],
"gasUsed": "0x0",
"currentBaseFee": "0x10",
"withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"currentExcessBlobGas": "0x0",
"blobGasUsed": "0x0",
"requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"requests": []
}
}

1
cmd/evm/testdata/34/txs.json vendored Normal file
View file

@ -0,0 +1 @@
[]

View file

@ -59,6 +59,8 @@ var (
Flags: slices.Concat([]cli.Flag{
utils.CachePreimagesFlag,
utils.OverrideOsaka,
utils.OverrideBPO1,
utils.OverrideBPO2,
utils.OverrideVerkle,
}, utils.DatabaseFlags),
Description: `
@ -274,6 +276,14 @@ func initGenesis(ctx *cli.Context) error {
v := ctx.Uint64(utils.OverrideOsaka.Name)
overrides.OverrideOsaka = &v
}
if ctx.IsSet(utils.OverrideBPO1.Name) {
v := ctx.Uint64(utils.OverrideBPO1.Name)
overrides.OverrideBPO1 = &v
}
if ctx.IsSet(utils.OverrideBPO2.Name) {
v := ctx.Uint64(utils.OverrideBPO2.Name)
overrides.OverrideBPO2 = &v
}
if ctx.IsSet(utils.OverrideVerkle.Name) {
v := ctx.Uint64(utils.OverrideVerkle.Name)
overrides.OverrideVerkle = &v

View file

@ -227,6 +227,14 @@ func makeFullNode(ctx *cli.Context) *node.Node {
v := ctx.Uint64(utils.OverrideOsaka.Name)
cfg.Eth.OverrideOsaka = &v
}
if ctx.IsSet(utils.OverrideBPO1.Name) {
v := ctx.Uint64(utils.OverrideBPO1.Name)
cfg.Eth.OverrideBPO1 = &v
}
if ctx.IsSet(utils.OverrideBPO2.Name) {
v := ctx.Uint64(utils.OverrideBPO2.Name)
cfg.Eth.OverrideBPO2 = &v
}
if ctx.IsSet(utils.OverrideVerkle.Name) {
v := ctx.Uint64(utils.OverrideVerkle.Name)
cfg.Eth.OverrideVerkle = &v

View file

@ -63,7 +63,10 @@ var (
utils.USBFlag,
utils.SmartCardDaemonPathFlag,
utils.OverrideOsaka,
utils.OverrideBPO1,
utils.OverrideBPO2,
utils.OverrideVerkle,
utils.OverrideGenesisFlag,
utils.EnablePersonal, // deprecated
utils.TxPoolLocalsFlag,
utils.TxPoolNoLocalsFlag,
@ -182,9 +185,12 @@ var (
utils.RPCGlobalGasCapFlag,
utils.RPCGlobalEVMTimeoutFlag,
utils.RPCGlobalTxFeeCapFlag,
utils.RPCGlobalLogQueryLimit,
utils.AllowUnprotectedTxs,
utils.BatchRequestLimit,
utils.BatchResponseMaxSize,
utils.RPCTxSyncDefaultTimeoutFlag,
utils.RPCTxSyncMaxTimeoutFlag,
}
metricsFlags = []cli.Flag{

View file

@ -561,17 +561,11 @@ func dumpState(ctx *cli.Context) error {
triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
defer triedb.Close()
snapConfig := snapshot.Config{
CacheSize: 256,
Recovery: false,
NoBuild: true,
AsyncBuild: false,
}
snaptree, err := snapshot.New(snapConfig, db, triedb, root)
stateIt, err := utils.NewStateIterator(triedb, db, root)
if err != nil {
return err
}
accIt, err := snaptree.AccountIterator(root, common.BytesToHash(conf.Start))
accIt, err := stateIt.AccountIterator(root, common.BytesToHash(conf.Start))
if err != nil {
return err
}
@ -605,7 +599,7 @@ func dumpState(ctx *cli.Context) error {
if !conf.SkipStorage {
da.Storage = make(map[common.Hash]string)
stIt, err := snaptree.StorageIterator(root, accIt.Hash(), common.Hash{})
stIt, err := stateIt.StorageIterator(root, accIt.Hash(), common.Hash{})
if err != nil {
return err
}
@ -658,17 +652,11 @@ func snapshotExportPreimages(ctx *cli.Context) error {
}
root = headBlock.Root()
}
snapConfig := snapshot.Config{
CacheSize: 256,
Recovery: false,
NoBuild: true,
AsyncBuild: false,
}
snaptree, err := snapshot.New(snapConfig, chaindb, triedb, root)
stateIt, err := utils.NewStateIterator(triedb, chaindb, root)
if err != nil {
return err
}
return utils.ExportSnapshotPreimages(chaindb, snaptree, ctx.Args().First(), root)
return utils.ExportSnapshotPreimages(chaindb, stateIt, ctx.Args().First(), root)
}
// checkAccount iterates the snap data layers, and looks up the given account

View file

@ -201,7 +201,7 @@ func expandVerkle(ctx *cli.Context) error {
}
for i, key := range keylist {
log.Info("Reading key", "index", i, "key", keylist[0])
log.Info("Reading key", "index", i, "key", key)
root.Get(key, chaindb.Get)
}

View file

@ -19,7 +19,7 @@
package main
import (
zkruntime "github.com/zkMIPS/zkMIPS/crates/go-runtime/zkm_runtime"
zkruntime "github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime"
)
// getInput reads the input payload from the zkVM runtime environment.

View file

@ -3,27 +3,28 @@ module github.com/ethereum/go-ethereum/cmd/keeper
go 1.24.0
require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6
github.com/ethereum/go-ethereum v0.0.0-00010101000000-000000000000
github.com/zkMIPS/zkMIPS/crates/go-runtime/zkm_runtime v0.0.0-20250915074013-fbc07aa2c6f5
)
require (
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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/emicklei/dot v1.6.2 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/ferranbt/fastssz v0.1.4 // indirect
github.com/go-ole/go-ole v1.3.0 // 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/uint256 v1.3.2 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
@ -33,16 +34,13 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/supranational/blst v0.3.14 // indirect
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/sys v0.36.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace (
github.com/ethereum/go-ethereum => ../../
github.com/zkMIPS/zkMIPS/crates/go-runtime/zkm_runtime => github.com/weilzkm/zkMIPS/crates/go-runtime/zkvm_runtime v0.0.0-20250915074013-fbc07aa2c6f5
)
replace github.com/ethereum/go-ethereum => ../../

View file

@ -1,16 +1,17 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
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/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
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/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
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/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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
@ -27,11 +28,10 @@ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAK
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0=
github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI=
github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
@ -42,8 +42,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU=
github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
@ -59,9 +61,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
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/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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 v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/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/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
@ -110,18 +111,14 @@ 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/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/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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/weilzkm/zkMIPS/crates/go-runtime/zkvm_runtime v0.0.0-20250915074013-fbc07aa2c6f5 h1:MxKlbmI7Dta6O6Nsc9OAer/rOltjoL11CVLMqCiYnxU=
github.com/weilzkm/zkMIPS/crates/go-runtime/zkvm_runtime v0.0.0-20250915074013-fbc07aa2c6f5/go.mod h1:zk/SUgiiVz2U1ufZ+yM2MHPbD93W25KH5zK3qAxXbT4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
@ -132,9 +129,8 @@ 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.8.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
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/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=

View file

@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"io"
"math"
"math/big"
"os"
"os/signal"
@ -48,6 +49,7 @@ import (
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/triedb"
"github.com/urfave/cli/v2"
)
@ -310,7 +312,7 @@ func ImportHistory(chain *core.BlockChain, dir string, network string) error {
return fmt.Errorf("error reading receipts %d: %w", it.Number(), err)
}
encReceipts := types.EncodeBlockReceiptLists([]types.Receipts{receipts})
if _, err := chain.InsertReceiptChain([]*types.Block{block}, encReceipts, 2^64-1); err != nil {
if _, err := chain.InsertReceiptChain([]*types.Block{block}, encReceipts, math.MaxUint64); err != nil {
return fmt.Errorf("error inserting body %d: %w", it.Number(), err)
}
imported += 1
@ -567,9 +569,64 @@ func ExportPreimages(db ethdb.Database, fn string) error {
return nil
}
// StateIterator is a temporary structure for traversing state in order. It serves
// as an aggregator for both path scheme and hash scheme implementations and should
// be removed once the hash scheme is fully deprecated.
type StateIterator struct {
scheme string
root common.Hash
triedb *triedb.Database
snapshots *snapshot.Tree
}
// NewStateIterator constructs the state iterator with the specific root.
func NewStateIterator(triedb *triedb.Database, db ethdb.Database, root common.Hash) (*StateIterator, error) {
if triedb.Scheme() == rawdb.PathScheme {
return &StateIterator{
scheme: rawdb.PathScheme,
root: root,
triedb: triedb,
}, nil
}
config := snapshot.Config{
CacheSize: 256,
Recovery: false,
NoBuild: true,
AsyncBuild: false,
}
snapshots, err := snapshot.New(config, db, triedb, root)
if err != nil {
return nil, err
}
return &StateIterator{
scheme: rawdb.HashScheme,
root: root,
triedb: triedb,
snapshots: snapshots,
}, nil
}
// AccountIterator creates a new account iterator for the specified root hash and
// seeks to a starting account hash.
func (it *StateIterator) AccountIterator(root common.Hash, start common.Hash) (snapshot.AccountIterator, error) {
if it.scheme == rawdb.PathScheme {
return it.triedb.AccountIterator(root, start)
}
return it.snapshots.AccountIterator(root, start)
}
// StorageIterator creates a new storage iterator for the specified root hash and
// account. The iterator will be moved to the specific start position.
func (it *StateIterator) StorageIterator(root common.Hash, accountHash common.Hash, start common.Hash) (snapshot.StorageIterator, error) {
if it.scheme == rawdb.PathScheme {
return it.triedb.StorageIterator(root, accountHash, start)
}
return it.snapshots.StorageIterator(root, accountHash, start)
}
// ExportSnapshotPreimages exports the preimages corresponding to the enumeration of
// the snapshot for a given root.
func ExportSnapshotPreimages(chaindb ethdb.Database, snaptree *snapshot.Tree, fn string, root common.Hash) error {
func ExportSnapshotPreimages(chaindb ethdb.Database, stateIt *StateIterator, fn string, root common.Hash) error {
log.Info("Exporting preimages", "file", fn)
fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
@ -602,7 +659,7 @@ func ExportSnapshotPreimages(chaindb ethdb.Database, snaptree *snapshot.Tree, fn
)
go func() {
defer close(hashCh)
accIt, err := snaptree.AccountIterator(root, common.Hash{})
accIt, err := stateIt.AccountIterator(root, common.Hash{})
if err != nil {
log.Error("Failed to create account iterator", "error", err)
return
@ -619,7 +676,7 @@ func ExportSnapshotPreimages(chaindb ethdb.Database, snaptree *snapshot.Tree, fn
hashCh <- hashAndPreimageSize{Hash: accIt.Hash(), Size: common.AddressLength}
if acc.Root != (common.Hash{}) && acc.Root != types.EmptyRootHash {
stIt, err := snaptree.StorageIterator(root, accIt.Hash(), common.Hash{})
stIt, err := stateIt.StorageIterator(root, accIt.Hash(), common.Hash{})
if err != nil {
log.Error("Failed to create storage iterator", "error", err)
return

View file

@ -20,11 +20,9 @@ package utils
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"net"
"net/http"
@ -248,11 +246,26 @@ var (
Usage: "Manually specify the Osaka fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideBPO1 = &cli.Uint64Flag{
Name: "override.bpo1",
Usage: "Manually specify the bpo1 fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideBPO2 = &cli.Uint64Flag{
Name: "override.bpo2",
Usage: "Manually specify the bpo2 fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideVerkle = &cli.Uint64Flag{
Name: "override.verkle",
Usage: "Manually specify the Verkle fork timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}
OverrideGenesisFlag = &cli.StringFlag{
Name: "override.genesis",
Usage: "Load genesis block and configuration from file at this path",
Category: flags.EthCategory,
}
SyncModeFlag = &cli.StringFlag{
Name: "syncmode",
Usage: `Blockchain sync mode ("snap" or "full")`,
@ -600,6 +613,24 @@ var (
Value: ethconfig.Defaults.RPCTxFeeCap,
Category: flags.APICategory,
}
RPCGlobalLogQueryLimit = &cli.IntFlag{
Name: "rpc.logquerylimit",
Usage: "Maximum number of alternative addresses or topics allowed per search position in eth_getLogs filter criteria (0 = no cap)",
Value: ethconfig.Defaults.LogQueryLimit,
Category: flags.APICategory,
}
RPCTxSyncDefaultTimeoutFlag = &cli.DurationFlag{
Name: "rpc.txsync.defaulttimeout",
Usage: "Default timeout for eth_sendRawTransactionSync (e.g. 2s, 500ms)",
Value: ethconfig.Defaults.TxSyncDefaultTimeout,
Category: flags.APICategory,
}
RPCTxSyncMaxTimeoutFlag = &cli.DurationFlag{
Name: "rpc.txsync.maxtimeout",
Usage: "Maximum allowed timeout for eth_sendRawTransactionSync (e.g. 5m)",
Value: ethconfig.Defaults.TxSyncMaxTimeout,
Category: flags.APICategory,
}
// Authenticated RPC HTTP settings
AuthListenFlag = &cli.StringFlag{
Name: "authrpc.addr",
@ -1309,15 +1340,10 @@ func setEtherbase(ctx *cli.Context, cfg *ethconfig.Config) {
return
}
addr := ctx.String(MinerPendingFeeRecipientFlag.Name)
if strings.HasPrefix(addr, "0x") || strings.HasPrefix(addr, "0X") {
addr = addr[2:]
}
b, err := hex.DecodeString(addr)
if err != nil || len(b) != common.AddressLength {
if !common.IsHexAddress(addr) {
Fatalf("-%s: invalid pending block producer address %q", MinerPendingFeeRecipientFlag.Name, addr)
return
}
cfg.Miner.PendingFeeRecipient = common.BytesToAddress(b)
cfg.Miner.PendingFeeRecipient = common.HexToAddress(addr)
}
func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) {
@ -1578,7 +1604,7 @@ func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) {
// SetEthConfig applies eth-related command line flags to the config.
func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
// Avoid conflicting network flags, don't allow network id override on preset networks
flags.CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag, HoodiFlag, NetworkIdFlag)
flags.CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag, HoodiFlag, NetworkIdFlag, OverrideGenesisFlag)
flags.CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer
// Set configurations from CLI flags
@ -1604,7 +1630,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
}
// Ensure Go's GC ignores the database cache for trigger percentage
cache := ctx.Int(CacheFlag.Name)
gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))
gogc := max(20, min(100, 100/(float64(cache)/1024)))
log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc))
godebug.SetGCPercent(int(gogc))
@ -1699,6 +1725,15 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(CacheLogSizeFlag.Name) {
cfg.FilterLogCacheSize = ctx.Int(CacheLogSizeFlag.Name)
}
if ctx.IsSet(RPCGlobalLogQueryLimit.Name) {
cfg.LogQueryLimit = ctx.Int(RPCGlobalLogQueryLimit.Name)
}
if ctx.IsSet(RPCTxSyncDefaultTimeoutFlag.Name) {
cfg.TxSyncDefaultTimeout = ctx.Duration(RPCTxSyncDefaultTimeoutFlag.Name)
}
if ctx.IsSet(RPCTxSyncMaxTimeoutFlag.Name) {
cfg.TxSyncMaxTimeout = ctx.Duration(RPCTxSyncMaxTimeoutFlag.Name)
}
if !ctx.Bool(SnapshotFlag.Name) || cfg.SnapshotCache == 0 {
// If snap-sync is requested, this flag is also required
if cfg.SyncMode == ethconfig.SnapSync {
@ -1855,6 +1890,18 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if !ctx.IsSet(MinerGasPriceFlag.Name) {
cfg.Miner.GasPrice = big.NewInt(1)
}
case ctx.String(OverrideGenesisFlag.Name) != "":
f, err := os.Open(ctx.String(OverrideGenesisFlag.Name))
if err != nil {
Fatalf("Failed to read genesis file: %v", err)
}
defer f.Close()
genesis := new(core.Genesis)
if err := json.NewDecoder(f).Decode(genesis); err != nil {
Fatalf("Invalid genesis file: %v", err)
}
cfg.Genesis = genesis
default:
if cfg.NetworkId == 1 {
SetDNSDiscoveryDefaults(cfg, params.MainnetGenesisHash)
@ -1922,11 +1969,15 @@ func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig {
} else {
Fatalf("Could not parse --%s: %v", BeaconGenesisRootFlag.Name, err)
}
configFile := ctx.String(BeaconConfigFlag.Name)
if err := config.ChainConfig.LoadForks(configFile); err != nil {
Fatalf("Could not load beacon chain config '%s': %v", configFile, err)
configPath := ctx.String(BeaconConfigFlag.Name)
file, err := os.ReadFile(configPath)
if err != nil {
Fatalf("failed to read beacon chain config file '%s': %v", configPath, err)
}
log.Info("Using custom beacon chain config", "file", configFile)
if err := config.ChainConfig.LoadForks(file); err != nil {
Fatalf("Could not load beacon chain config '%s': %v", configPath, err)
}
log.Info("Using custom beacon chain config", "file", configPath)
} else {
if ctx.IsSet(BeaconGenesisRootFlag.Name) {
Fatalf("Genesis root is specified but custom beacon chain config is missing")
@ -2017,7 +2068,8 @@ func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, filterSyst
// RegisterFilterAPI adds the eth log filtering RPC API to the node.
func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconfig.Config) *filters.FilterSystem {
filterSystem := filters.NewFilterSystem(backend, filters.Config{
LogCacheSize: ethcfg.FilterLogCacheSize,
LogCacheSize: ethcfg.FilterLogCacheSize,
LogQueryLimit: ethcfg.LogQueryLimit,
})
stack.RegisterAPIs([]rpc.API{{
Namespace: "eth",

View file

@ -182,13 +182,14 @@ func (s *filterTestSuite) loadQueries() error {
// filterQuery is a single query for testing.
type filterQuery struct {
FromBlock int64 `json:"fromBlock"`
ToBlock int64 `json:"toBlock"`
Address []common.Address `json:"address"`
Topics [][]common.Hash `json:"topics"`
ResultHash *common.Hash `json:"resultHash,omitempty"`
results []types.Log
Err error `json:"error,omitempty"`
FromBlock int64 `json:"fromBlock"`
ToBlock int64 `json:"toBlock"`
lastBlockHash common.Hash
Address []common.Address `json:"address"`
Topics [][]common.Hash `json:"topics"`
ResultHash *common.Hash `json:"resultHash,omitempty"`
results []types.Log
Err error `json:"error,omitempty"`
}
func (fq *filterQuery) isWildcard() bool {

View file

@ -0,0 +1,337 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"context"
"fmt"
"math/big"
"reflect"
"slices"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
const maxFilterRangeForTestFuzz = 300
var (
filterFuzzCommand = &cli.Command{
Name: "filterfuzz",
Usage: "Generates queries and compares results against matches derived from receipts",
ArgsUsage: "<RPC endpoint URL>",
Action: filterFuzzCmd,
Flags: []cli.Flag{},
}
)
// filterFuzzCmd is the main function of the filter fuzzer.
func filterFuzzCmd(ctx *cli.Context) error {
f := newFilterTestGen(ctx, maxFilterRangeForTestFuzz)
var lastHead *types.Header
headerCache := lru.NewCache[common.Hash, *types.Header](200)
commonAncestor := func(oldPtr, newPtr *types.Header) *types.Header {
if oldPtr == nil || newPtr == nil {
return nil
}
if newPtr.Number.Uint64() > oldPtr.Number.Uint64()+100 || oldPtr.Number.Uint64() > newPtr.Number.Uint64()+100 {
return nil
}
for oldPtr.Hash() != newPtr.Hash() {
if newPtr.Number.Uint64() >= oldPtr.Number.Uint64() {
if parent, _ := headerCache.Get(newPtr.ParentHash); parent != nil {
newPtr = parent
} else {
newPtr, _ = getHeaderByHash(f.client, newPtr.ParentHash)
if newPtr == nil {
return nil
}
headerCache.Add(newPtr.Hash(), newPtr)
}
}
if oldPtr.Number.Uint64() > newPtr.Number.Uint64() {
oldPtr, _ = headerCache.Get(oldPtr.ParentHash)
if oldPtr == nil {
return nil
}
}
}
return newPtr
}
fetchHead := func() (*types.Header, bool) {
currentHead, err := getLatestHeader(f.client)
if err != nil {
fmt.Println("Could not fetch head block", err)
return nil, false
}
headerCache.Add(currentHead.Hash(), currentHead)
if lastHead != nil && currentHead.Hash() == lastHead.Hash() {
return currentHead, false
}
f.blockLimit = currentHead.Number.Int64()
ca := commonAncestor(lastHead, currentHead)
fmt.Print("*** New head ", f.blockLimit)
if ca == nil {
fmt.Println(" <no common ancestor>")
} else {
if reorged := lastHead.Number.Uint64() - ca.Number.Uint64(); reorged > 0 {
fmt.Print(" reorged ", reorged)
}
if missed := currentHead.Number.Uint64() - ca.Number.Uint64() - 1; missed > 0 {
fmt.Print(" missed ", missed)
}
fmt.Println()
}
lastHead = currentHead
return currentHead, true
}
tryExtendQuery := func(query *filterQuery) *filterQuery {
for {
extQuery := f.extendRange(query)
if extQuery == nil {
return query
}
extQuery.checkLastBlockHash(f.client)
extQuery.run(f.client, nil)
if extQuery.Err == nil && len(extQuery.results) == 0 {
// query is useless now due to major reorg; abandon and continue
fmt.Println("Zero length results")
return nil
}
if extQuery.Err != nil {
extQuery.printError()
return nil
}
if len(extQuery.results) > maxFilterResultSize {
return query
}
query = extQuery
}
}
var (
mmQuery *filterQuery
mmRetry, mmNextRetry int
)
mainLoop:
for {
select {
case <-ctx.Done():
return nil
default:
}
var query *filterQuery
if mmQuery != nil {
if mmRetry == 0 {
query = mmQuery
mmRetry = mmNextRetry
mmNextRetry *= 2
query.checkLastBlockHash(f.client)
query.run(f.client, nil)
if query.Err != nil {
query.printError()
continue
}
fmt.Println("Retrying query from:", query.FromBlock, "to:", query.ToBlock, "results:", len(query.results))
} else {
mmRetry--
}
}
if query == nil {
currentHead, isNewHead := fetchHead()
if currentHead == nil {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Second):
}
continue mainLoop
}
if isNewHead {
query = f.newHeadSeedQuery(currentHead.Number.Int64())
} else {
query = f.newQuery()
}
query.checkLastBlockHash(f.client)
query.run(f.client, nil)
if query.Err != nil {
query.printError()
continue
}
fmt.Println("New query from:", query.FromBlock, "to:", query.ToBlock, "results:", len(query.results))
if len(query.results) == 0 || len(query.results) > maxFilterResultSize {
continue mainLoop
}
if query = tryExtendQuery(query); query == nil {
continue mainLoop
}
}
if !query.checkLastBlockHash(f.client) {
fmt.Println("Reorg during search")
continue mainLoop
}
// now we have a new query; check results
results, err := query.getResultsFromReceipts(f.client)
if err != nil {
fmt.Println("Could not fetch results from receipts", err)
continue mainLoop
}
if !query.checkLastBlockHash(f.client) {
fmt.Println("Reorg during search")
continue mainLoop
}
if !reflect.DeepEqual(query.results, results) {
fmt.Println("Results mismatch from:", query.FromBlock, "to:", query.ToBlock, "addresses:", query.Address, "topics:", query.Topics)
resShared, resGetLogs, resReceipts := compareResults(query.results, results)
fmt.Println(" shared:", len(resShared))
fmt.Println(" only from getLogs:", len(resGetLogs), resGetLogs)
fmt.Println(" only from receipts:", len(resReceipts), resReceipts)
if mmQuery != query {
mmQuery = query
mmRetry = 0
mmNextRetry = 1
}
continue mainLoop
}
fmt.Println("Successful query from:", query.FromBlock, "to:", query.ToBlock, "results:", len(query.results))
f.storeQuery(query)
}
}
func compareResults(a, b []types.Log) (shared, onlya, onlyb []types.Log) {
for len(a) > 0 && len(b) > 0 {
if reflect.DeepEqual(a[0], b[0]) {
shared = append(shared, a[0])
a = a[1:]
b = b[1:]
} else {
for i := 1; ; i++ {
if i >= len(a) { // b[0] not found in a
onlyb = append(onlyb, b[0])
b = b[1:]
break
}
if i >= len(b) { // a[0] not found in b
onlya = append(onlya, a[0])
a = a[1:]
break
}
if reflect.DeepEqual(b[0], a[i]) { // a[:i] not found in b
onlya = append(onlya, a[:i]...)
a = a[i:]
break
}
if reflect.DeepEqual(a[0], b[i]) { // b[:i] not found in a
onlyb = append(onlyb, b[:i]...)
b = b[i:]
break
}
}
}
}
onlya = append(onlya, a...)
onlyb = append(onlyb, b...)
return
}
func getLatestHeader(client *client) (*types.Header, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return client.Eth.HeaderByNumber(ctx, big.NewInt(int64(rpc.LatestBlockNumber)))
}
func getHeaderByHash(client *client, hash common.Hash) (*types.Header, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return client.Eth.HeaderByHash(ctx, hash)
}
// newHeadSeedQuery creates a query that gets all logs from the latest head.
func (s *filterTestGen) newHeadSeedQuery(head int64) *filterQuery {
return &filterQuery{
FromBlock: head,
ToBlock: head,
}
}
func (fq *filterQuery) checkLastBlockHash(client *client) bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
header, err := client.Eth.HeaderByNumber(ctx, big.NewInt(fq.ToBlock))
if err != nil {
fmt.Println("Cound not fetch last block hash of query number:", fq.ToBlock, "error:", err)
fq.lastBlockHash = common.Hash{}
return false
}
hash := header.Hash()
if fq.lastBlockHash == hash {
return true
}
fq.lastBlockHash = hash
return false
}
func (fq *filterQuery) filterLog(log *types.Log) bool {
if len(fq.Address) > 0 && !slices.Contains(fq.Address, log.Address) {
return false
}
// If the to filtered topics is greater than the amount of topics in logs, skip.
if len(fq.Topics) > len(log.Topics) {
return false
}
for i, sub := range fq.Topics {
if len(sub) == 0 {
continue // empty rule set == wildcard
}
if !slices.Contains(sub, log.Topics[i]) {
return false
}
}
return true
}
func (fq *filterQuery) getResultsFromReceipts(client *client) ([]types.Log, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
var results []types.Log
for blockNumber := fq.FromBlock; blockNumber <= fq.ToBlock; blockNumber++ {
receipts, err := client.Eth.BlockReceipts(ctx, rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(blockNumber)))
if err != nil {
return nil, err
}
for _, receipt := range receipts {
for _, log := range receipt.Logs {
if fq.filterLog(log) {
results = append(results, *log)
}
}
}
}
return results, nil
}

View file

@ -32,6 +32,17 @@ import (
"github.com/urfave/cli/v2"
)
const (
// Parameter of the random filter query generator.
maxFilterRangeForTestGen = 100000000000
maxFilterResultSize = 1000
filterBuckets = 10
maxFilterBucketSize = 100
filterSeedChance = 10
filterMergeChance = 45
filterExtendChance = 50
)
var (
filterGenerateCommand = &cli.Command{
Name: "filtergen",
@ -58,7 +69,7 @@ var (
// filterGenCmd is the main function of the filter tests generator.
func filterGenCmd(ctx *cli.Context) error {
f := newFilterTestGen(ctx)
f := newFilterTestGen(ctx, maxFilterRangeForTestGen)
lastWrite := time.Now()
for {
select {
@ -67,7 +78,7 @@ func filterGenCmd(ctx *cli.Context) error {
default:
}
f.updateFinalizedBlock()
f.setLimitToFinalizedBlock()
query := f.newQuery()
query.run(f.client, nil)
if query.Err != nil {
@ -75,7 +86,7 @@ func filterGenCmd(ctx *cli.Context) error {
exit("filter query failed")
}
if len(query.results) > 0 && len(query.results) <= maxFilterResultSize {
for {
for rand.Intn(100) < filterExtendChance {
extQuery := f.extendRange(query)
if extQuery == nil {
break
@ -108,39 +119,32 @@ func filterGenCmd(ctx *cli.Context) error {
// filterTestGen is the filter query test generator.
type filterTestGen struct {
client *client
queryFile string
client *client
queryFile string
maxFilterRange int64
finalizedBlock int64
queries [filterBuckets][]*filterQuery
blockLimit int64
queries [filterBuckets][]*filterQuery
}
func newFilterTestGen(ctx *cli.Context) *filterTestGen {
func newFilterTestGen(ctx *cli.Context, maxFilterRange int64) *filterTestGen {
return &filterTestGen{
client: makeClient(ctx),
queryFile: ctx.String(filterQueryFileFlag.Name),
client: makeClient(ctx),
queryFile: ctx.String(filterQueryFileFlag.Name),
maxFilterRange: maxFilterRange,
}
}
func (s *filterTestGen) updateFinalizedBlock() {
s.finalizedBlock = mustGetFinalizedBlock(s.client)
func (s *filterTestGen) setLimitToFinalizedBlock() {
s.blockLimit = mustGetFinalizedBlock(s.client)
}
const (
// Parameter of the random filter query generator.
maxFilterRange = 10000000
maxFilterResultSize = 300
filterBuckets = 10
maxFilterBucketSize = 100
filterSeedChance = 10
filterMergeChance = 45
)
// storeQuery adds a filter query to the output file.
func (s *filterTestGen) storeQuery(query *filterQuery) {
query.ResultHash = new(common.Hash)
*query.ResultHash = query.calculateHash()
logRatio := math.Log(float64(len(query.results))*float64(s.finalizedBlock)/float64(query.ToBlock+1-query.FromBlock)) / math.Log(float64(s.finalizedBlock)*maxFilterResultSize)
maxFilterRange := min(s.maxFilterRange, s.blockLimit)
logRatio := math.Log(float64(len(query.results))*float64(maxFilterRange)/float64(query.ToBlock+1-query.FromBlock)) / math.Log(float64(maxFilterRange)*maxFilterResultSize)
bucket := int(math.Floor(logRatio * filterBuckets))
if bucket >= filterBuckets {
bucket = filterBuckets - 1
@ -160,13 +164,13 @@ func (s *filterTestGen) storeQuery(query *filterQuery) {
func (s *filterTestGen) extendRange(q *filterQuery) *filterQuery {
rangeLen := q.ToBlock + 1 - q.FromBlock
extLen := rand.Int63n(rangeLen) + 1
if rangeLen+extLen > s.finalizedBlock {
if rangeLen+extLen > min(s.maxFilterRange, s.blockLimit) {
return nil
}
extBefore := min(rand.Int63n(extLen+1), q.FromBlock)
extAfter := extLen - extBefore
if q.ToBlock+extAfter > s.finalizedBlock {
d := q.ToBlock + extAfter - s.finalizedBlock
if q.ToBlock+extAfter > s.blockLimit {
d := q.ToBlock + extAfter - s.blockLimit
extAfter -= d
if extBefore+d <= q.FromBlock {
extBefore += d
@ -203,7 +207,7 @@ func (s *filterTestGen) newQuery() *filterQuery {
// newSeedQuery creates a query that gets all logs in a random non-finalized block.
func (s *filterTestGen) newSeedQuery() *filterQuery {
block := rand.Int63n(s.finalizedBlock + 1)
block := rand.Int63n(s.blockLimit + 1)
return &filterQuery{
FromBlock: block,
ToBlock: block,
@ -358,6 +362,7 @@ func (s *filterTestGen) writeQueries() {
func mustGetFinalizedBlock(client *client) int64 {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
header, err := client.Eth.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber)))
if err != nil {
exit(fmt.Errorf("could not fetch finalized header (error: %v)", err))

View file

@ -49,6 +49,7 @@ func init() {
filterGenerateCommand,
traceGenerateCommand,
filterPerfCommand,
filterFuzzCommand,
}
}

View file

@ -408,7 +408,6 @@ func TestUnmarshalFixedUnprefixedText(t *testing.T) {
{input: "0x2", wantErr: ErrOddLength},
{input: "2", wantErr: ErrOddLength},
{input: "4444", wantErr: errors.New("hex string has length 4, want 8 for x")},
{input: "4444", wantErr: errors.New("hex string has length 4, want 8 for x")},
// check that output is not modified for partially correct input
{input: "444444gg", wantErr: ErrSyntax, want: []byte{0, 0, 0, 0}},
{input: "0x444444gg", wantErr: ErrSyntax, want: []byte{0, 0, 0, 0}},

View file

@ -110,7 +110,7 @@ func TestMustParseUint64(t *testing.T) {
func TestMustParseUint64Panic(t *testing.T) {
defer func() {
if recover() == nil {
t.Error("MustParseBig should've panicked")
t.Error("MustParseUint64 should've panicked")
}
}()
MustParseUint64("ggg")

View file

@ -71,16 +71,6 @@ func New(ethone consensus.Engine) *Beacon {
return &Beacon{ethone: ethone}
}
// isPostMerge reports whether the given block number is assumed to be post-merge.
// Here we check the MergeNetsplitBlock to allow configuring networks with a PoW or
// PoA chain for unit testing purposes.
func isPostMerge(config *params.ChainConfig, blockNum uint64, timestamp uint64) bool {
mergedAtGenesis := config.TerminalTotalDifficulty != nil && config.TerminalTotalDifficulty.Sign() == 0
return mergedAtGenesis ||
config.MergeNetsplitBlock != nil && blockNum >= config.MergeNetsplitBlock.Uint64() ||
config.ShanghaiTime != nil && timestamp >= *config.ShanghaiTime
}
// Author implements consensus.Engine, returning the verified author of the block.
func (beacon *Beacon) Author(header *types.Header) (common.Address, error) {
if !beacon.IsPoSHeader(header) {
@ -328,7 +318,7 @@ func (beacon *Beacon) verifyHeaders(chain consensus.ChainHeaderReader, headers [
// Prepare implements consensus.Engine, initializing the difficulty field of a
// header to conform to the beacon protocol. The changes are done inline.
func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {
if !isPostMerge(chain.Config(), header.Number.Uint64(), header.Time) {
if !chain.Config().IsPostMerge(header.Number.Uint64(), header.Time) {
return beacon.ethone.Prepare(chain, header)
}
header.Difficulty = beaconDifficulty
@ -442,7 +432,7 @@ func (beacon *Beacon) SealHash(header *types.Header) common.Hash {
// the difficulty that a new block should have when created at time
// given the parent block's time and difficulty.
func (beacon *Beacon) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
if !isPostMerge(chain.Config(), parent.Number.Uint64()+1, time) {
if !chain.Config().IsPostMerge(parent.Number.Uint64()+1, time) {
return beacon.ethone.CalcDifficulty(chain, time, parent)
}
return beaconDifficulty

View file

@ -301,7 +301,7 @@ func makeChainForBench(db ethdb.Database, genesis *Genesis, full bool, count uin
func benchWriteChain(b *testing.B, full bool, count uint64) {
genesis := &Genesis{Config: params.AllEthashProtocolChanges}
for i := 0; i < b.N; i++ {
for b.Loop() {
pdb, err := pebble.New(b.TempDir(), 1024, 128, "", false)
if err != nil {
b.Fatalf("error opening database: %v", err)
@ -326,9 +326,7 @@ func benchReadChain(b *testing.B, full bool, count uint64) {
db.Close()
options := DefaultConfig().WithArchive(true)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
pdb, err = pebble.New(dir, 1024, 128, "", false)
if err != nil {
b.Fatalf("error opening database: %v", err)

View file

@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io"
"math"
"math/big"
"runtime"
"slices"
@ -395,7 +394,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
bc.statedb = state.NewDatabase(bc.triedb, nil)
bc.validator = NewBlockValidator(chainConfig, bc)
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
bc.processor = NewStateProcessor(chainConfig, bc.hc)
bc.processor = NewStateProcessor(bc.hc)
genesisHeader := bc.GetHeaderByNumber(0)
if genesisHeader == nil {
@ -1691,7 +1690,12 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types
// Set new head.
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 {
bc.logsFeed.Send(logs)
}
@ -2343,6 +2347,13 @@ func (bc *BlockChain) recoverAncestors(block *types.Block, makeWitness bool) (co
// collectLogs collects the logs that were generated or removed during the
// processing of a block. These logs are later announced as deleted or reborn.
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
if b.ExcessBlobGas() != nil {
blobGasPrice = eip4844.CalcBlobFee(bc.chainConfig, b.Header())
@ -2360,7 +2371,7 @@ func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.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
@ -2589,8 +2600,14 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
bc.writeHeadBlock(head)
// Emit events
logs := bc.collectLogs(head, false)
bc.chainFeed.Send(ChainEvent{Header: head.Header()})
receipts, logs := bc.collectReceiptsAndLogs(head, false)
bc.chainFeed.Send(ChainEvent{
Header: head.Header(),
Receipts: receipts,
Transactions: head.Transactions(),
})
if len(logs) > 0 {
bc.logsFeed.Send(logs)
}
@ -2663,13 +2680,11 @@ func (bc *BlockChain) reportBlock(block *types.Block, res *ProcessResult, err er
// logForkReadiness will write a log when a future fork is scheduled, but not
// active. This is useful so operators know their client is ready for the fork.
func (bc *BlockChain) logForkReadiness(block *types.Block) {
config := bc.Config()
current, last := config.LatestFork(block.Time()), config.LatestFork(math.MaxUint64)
current := bc.Config().LatestFork(block.Time())
// Short circuit if the timestamp of the last fork is undefined,
// or if the network has already passed the last configured fork.
t := config.Timestamp(last)
if t == nil || current >= last {
// Short circuit if the timestamp of the last fork is undefined.
t := bc.Config().Timestamp(current + 1)
if t == nil {
return
}
at := time.Unix(int64(*t), 0)
@ -2679,7 +2694,7 @@ func (bc *BlockChain) logForkReadiness(block *types.Block) {
// - Enough time has passed since last alert
now := time.Now()
if now.Before(at) && now.After(bc.lastForkReadyAlert.Add(forkReadyInterval)) {
log.Info("Ready for fork activation", "fork", last, "date", at.Format(time.RFC822),
log.Info("Ready for fork activation", "fork", current+1, "date", at.Format(time.RFC822),
"remaining", time.Until(at).Round(time.Second), "timestamp", at.Unix())
bc.lastForkReadyAlert = time.Now()
}

View file

@ -27,7 +27,9 @@ type NewTxsEvent struct{ Txs []*types.Transaction }
type RemovedLogsEvent struct{ Logs []*types.Log }
type ChainEvent struct {
Header *types.Header
Header *types.Header
Receipts []*types.Receipt
Transactions []*types.Transaction
}
type ChainHeadEvent struct {

View file

@ -25,21 +25,16 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// ChainContext supports retrieving headers and consensus parameters from the
// current blockchain to be used during transaction processing.
type ChainContext interface {
consensus.ChainHeaderReader
// Engine retrieves the chain's 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.

View file

@ -95,8 +95,14 @@ func TestCreation(t *testing.T) {
{1735372, 1706655071, ID{Hash: checksumToBytes(0xf7f9bc08), Next: 1706655072}}, // Last Shanghai block
{1735372, 1706655072, ID{Hash: checksumToBytes(0x88cf81d9), Next: 1741159776}}, // First Cancun block
{1735372, 1741159775, ID{Hash: checksumToBytes(0x88cf81d9), Next: 1741159776}}, // Last Cancun block
{1735372, 1741159776, ID{Hash: checksumToBytes(0xed88b5fd), Next: 0}}, // First Prague block
{1735372, 2741159776, ID{Hash: checksumToBytes(0xed88b5fd), Next: 0}}, // Future Prague block
{1735372, 1741159776, ID{Hash: checksumToBytes(0xed88b5fd), Next: 1760427360}}, // First Prague block
{1735372, 1760427359, ID{Hash: checksumToBytes(0xed88b5fd), Next: 1760427360}}, // Last Prague block
{1735372, 1760427360, ID{Hash: checksumToBytes(0xe2ae4999), Next: 1761017184}}, // First Osaka block
{1735372, 1761017183, ID{Hash: checksumToBytes(0xe2ae4999), Next: 1761017184}}, // Last Osaka block
{1735372, 1761017184, ID{Hash: checksumToBytes(0x56078a1e), Next: 1761607008}}, // First BPO1 block
{1735372, 1761607007, ID{Hash: checksumToBytes(0x56078a1e), Next: 1761607008}}, // Last BPO1 block
{1735372, 1761607008, ID{Hash: checksumToBytes(0x268956b6), Next: 0}}, // First BPO2 block
{1735372, 2000000000, ID{Hash: checksumToBytes(0x268956b6), Next: 0}}, // Future BPO2 block
},
},
// Holesky test cases
@ -110,8 +116,14 @@ func TestCreation(t *testing.T) {
{123, 1707305663, ID{Hash: checksumToBytes(0xfd4f016b), Next: 1707305664}}, // Last Shanghai block
{123, 1707305664, ID{Hash: checksumToBytes(0x9b192ad0), Next: 1740434112}}, // First Cancun block
{123, 1740434111, ID{Hash: checksumToBytes(0x9b192ad0), Next: 1740434112}}, // Last Cancun block
{123, 1740434112, ID{Hash: checksumToBytes(0xdfbd9bed), Next: 0}}, // First Prague block
{123, 2740434112, ID{Hash: checksumToBytes(0xdfbd9bed), Next: 0}}, // Future Prague block
{123, 1740434112, ID{Hash: checksumToBytes(0xdfbd9bed), Next: 1759308480}}, // First Prague block
{123, 1759308479, ID{Hash: checksumToBytes(0xdfbd9bed), Next: 1759308480}}, // Last Prague block
{123, 1759308480, ID{Hash: checksumToBytes(0x783def52), Next: 1759800000}}, // First Osaka block
{123, 1759799999, ID{Hash: checksumToBytes(0x783def52), Next: 1759800000}}, // Last Osaka block
{123, 1759800000, ID{Hash: checksumToBytes(0xa280a45c), Next: 1760389824}}, // First BPO1 block
{123, 1760389823, ID{Hash: checksumToBytes(0xa280a45c), Next: 1760389824}}, // Last BPO1 block
{123, 1760389824, ID{Hash: checksumToBytes(0x9bc6cb31), Next: 0}}, // First BPO2 block
{123, 2000000000, ID{Hash: checksumToBytes(0x9bc6cb31), Next: 0}}, // Future BPO1 block
},
},
// Hoodi test cases
@ -121,8 +133,14 @@ func TestCreation(t *testing.T) {
[]testcase{
{0, 0, ID{Hash: checksumToBytes(0xbef71d30), Next: 1742999832}}, // Unsynced, last Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople, Petersburg, Istanbul, Berlin, London, Paris, Shanghai, Cancun block
{123, 1742999831, ID{Hash: checksumToBytes(0xbef71d30), Next: 1742999832}}, // Last Cancun block
{123, 1742999832, ID{Hash: checksumToBytes(0x0929e24e), Next: 0}}, // First Prague block
{123, 2740434112, ID{Hash: checksumToBytes(0x0929e24e), Next: 0}}, // Future Prague block
{123, 1742999832, ID{Hash: checksumToBytes(0x0929e24e), Next: 1761677592}}, // First Prague block
{123, 1761677591, ID{Hash: checksumToBytes(0x0929e24e), Next: 1761677592}}, // Last Prague block
{123, 1761677592, ID{Hash: checksumToBytes(0xe7e0e7ff), Next: 1762365720}}, // First Osaka block
{123, 1762365719, ID{Hash: checksumToBytes(0xe7e0e7ff), Next: 1762365720}}, // Last Osaka block
{123, 1762365720, ID{Hash: checksumToBytes(0x3893353e), Next: 1762955544}}, // First BPO1 block
{123, 1762955543, ID{Hash: checksumToBytes(0x3893353e), Next: 1762955544}}, // Last BPO1 block
{123, 1762955544, ID{Hash: checksumToBytes(0x23aa1351), Next: 0}}, // First BPO2 block
{123, 2000000000, ID{Hash: checksumToBytes(0x23aa1351), Next: 0}}, // Future BPO2 block
},
},
}

View file

@ -259,6 +259,8 @@ func (e *GenesisMismatchError) Error() string {
// ChainOverrides contains the changes to chain config.
type ChainOverrides struct {
OverrideOsaka *uint64
OverrideBPO1 *uint64
OverrideBPO2 *uint64
OverrideVerkle *uint64
}
@ -270,6 +272,12 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error {
if o.OverrideOsaka != nil {
cfg.OsakaTime = o.OverrideOsaka
}
if o.OverrideBPO1 != nil {
cfg.BPO1Time = o.OverrideBPO1
}
if o.OverrideBPO2 != nil {
cfg.BPO2Time = o.OverrideBPO2
}
if o.OverrideVerkle != nil {
cfg.VerkleTime = o.OverrideVerkle
}
@ -514,6 +522,11 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
if head.BlobGasUsed == nil {
head.BlobGasUsed = new(uint64)
}
} else {
if g.ExcessBlobGas != nil {
log.Warn("Invalid genesis, unexpected ExcessBlobGas set before Cancun, allowing it for testing purposes")
head.ExcessBlobGas = g.ExcessBlobGas
}
}
if conf.IsPrague(num, g.Timestamp) {
head.RequestsHash = &types.EmptyRequestsHash
@ -656,23 +669,24 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
BaseFee: big.NewInt(params.InitialBaseFee),
Difficulty: big.NewInt(0),
Alloc: map[common.Address]types.Account{
common.BytesToAddress([]byte{0x01}): {Balance: big.NewInt(1)}, // ECRecover
common.BytesToAddress([]byte{0x02}): {Balance: big.NewInt(1)}, // SHA256
common.BytesToAddress([]byte{0x03}): {Balance: big.NewInt(1)}, // RIPEMD
common.BytesToAddress([]byte{0x04}): {Balance: big.NewInt(1)}, // Identity
common.BytesToAddress([]byte{0x05}): {Balance: big.NewInt(1)}, // ModExp
common.BytesToAddress([]byte{0x06}): {Balance: big.NewInt(1)}, // ECAdd
common.BytesToAddress([]byte{0x07}): {Balance: big.NewInt(1)}, // ECScalarMul
common.BytesToAddress([]byte{0x08}): {Balance: big.NewInt(1)}, // ECPairing
common.BytesToAddress([]byte{0x09}): {Balance: big.NewInt(1)}, // BLAKE2b
common.BytesToAddress([]byte{0x0a}): {Balance: big.NewInt(1)}, // KZGPointEval
common.BytesToAddress([]byte{0x0b}): {Balance: big.NewInt(1)}, // BLSG1Add
common.BytesToAddress([]byte{0x0c}): {Balance: big.NewInt(1)}, // BLSG1MultiExp
common.BytesToAddress([]byte{0x0d}): {Balance: big.NewInt(1)}, // BLSG2Add
common.BytesToAddress([]byte{0x0e}): {Balance: big.NewInt(1)}, // BLSG2MultiExp
common.BytesToAddress([]byte{0x0f}): {Balance: big.NewInt(1)}, // BLSG1Pairing
common.BytesToAddress([]byte{0x10}): {Balance: big.NewInt(1)}, // BLSG1MapG1
common.BytesToAddress([]byte{0x11}): {Balance: big.NewInt(1)}, // BLSG2MapG2
common.BytesToAddress([]byte{0x01}): {Balance: big.NewInt(1)}, // ECRecover
common.BytesToAddress([]byte{0x02}): {Balance: big.NewInt(1)}, // SHA256
common.BytesToAddress([]byte{0x03}): {Balance: big.NewInt(1)}, // RIPEMD
common.BytesToAddress([]byte{0x04}): {Balance: big.NewInt(1)}, // Identity
common.BytesToAddress([]byte{0x05}): {Balance: big.NewInt(1)}, // ModExp
common.BytesToAddress([]byte{0x06}): {Balance: big.NewInt(1)}, // ECAdd
common.BytesToAddress([]byte{0x07}): {Balance: big.NewInt(1)}, // ECScalarMul
common.BytesToAddress([]byte{0x08}): {Balance: big.NewInt(1)}, // ECPairing
common.BytesToAddress([]byte{0x09}): {Balance: big.NewInt(1)}, // BLAKE2b
common.BytesToAddress([]byte{0x0a}): {Balance: big.NewInt(1)}, // KZGPointEval
common.BytesToAddress([]byte{0x0b}): {Balance: big.NewInt(1)}, // BLSG1Add
common.BytesToAddress([]byte{0x0c}): {Balance: big.NewInt(1)}, // BLSG1MultiExp
common.BytesToAddress([]byte{0x0d}): {Balance: big.NewInt(1)}, // BLSG2Add
common.BytesToAddress([]byte{0x0e}): {Balance: big.NewInt(1)}, // BLSG2MultiExp
common.BytesToAddress([]byte{0x0f}): {Balance: big.NewInt(1)}, // BLSG1Pairing
common.BytesToAddress([]byte{0x10}): {Balance: big.NewInt(1)}, // BLSG1MapG1
common.BytesToAddress([]byte{0x11}): {Balance: big.NewInt(1)}, // BLSG2MapG2
common.BytesToAddress([]byte{0x1, 00}): {Balance: big.NewInt(1)}, // P256Verify
// Pre-deploy system contracts
params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0},
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},

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
// the list of logs. When decoding a stored receipt into this object we
// avoid creating the bloom filter.
@ -682,11 +673,11 @@ type receiptLogs struct {
// DecodeRLP implements rlp.Decoder.
func (r *receiptLogs) DecodeRLP(s *rlp.Stream) error {
var stored storedReceiptRLP
if err := s.Decode(&stored); err != nil {
var rs types.ReceiptForStorage
if err := rs.DecodeRLP(s); err != nil {
return err
}
r.Logs = stored.Logs
r.Logs = rs.Logs
return nil
}

View file

@ -246,7 +246,7 @@ func TestBadBlockStorage(t *testing.T) {
}
for i := 0; i < len(badBlocks)-1; i++ {
if badBlocks[i].NumberU64() < badBlocks[i+1].NumberU64() {
t.Fatalf("The bad blocks are not sorted #[%d](%d) < #[%d](%d)", i, i+1, badBlocks[i].NumberU64(), badBlocks[i+1].NumberU64())
t.Fatalf("The bad blocks are not sorted #[%d](%d) < #[%d](%d)", i, badBlocks[i].NumberU64(), i+1, badBlocks[i+1].NumberU64())
}
}

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
// account address.
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
// account address along with the block id.
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
// addition operation overflows.
func increaseKey(key []byte) []byte {
@ -155,14 +224,26 @@ func increaseKey(key []byte) []byte {
return nil
}
// DeleteStateHistoryIndex completely removes all history indexing data, including
// DeleteStateHistoryIndexes completely removes all history indexing data, including
// indexes for accounts and storages.
//
// Note, this method assumes the storage space with prefix `StateHistoryIndexPrefix`
// is exclusively occupied by the history indexing data!
func DeleteStateHistoryIndex(db ethdb.KeyValueRangeDeleter) {
start := StateHistoryIndexPrefix
limit := increaseKey(bytes.Clone(StateHistoryIndexPrefix))
func DeleteStateHistoryIndexes(db ethdb.KeyValueRangeDeleter) {
DeleteHistoryByRange(db, StateHistoryAccountMetadataPrefix)
DeleteHistoryByRange(db, StateHistoryStorageMetadataPrefix)
DeleteHistoryByRange(db, StateHistoryAccountBlockPrefix)
DeleteHistoryByRange(db, StateHistoryStorageBlockPrefix)
}
// 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
// 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)
}
// ReadStateAccountIndex retrieves the state root corresponding to the specified
// state history. 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).
// ReadStateAccountIndex retrieves the account index blob for the specified
// state history. The index contains fixed-size entries with offsets and lengths
// 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 {
blob, err := db.Ancient(stateHistoryAccountIndex, id-1)
if err != nil {
@ -181,37 +183,30 @@ func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte {
return blob
}
// ReadStateStorageIndex retrieves the state root corresponding to the specified
// state history. 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 {
blob, err := db.Ancient(stateHistoryStorageIndex, id-1)
if err != nil {
return nil
}
return blob
// ReadStateStorageIndex retrieves the storage index blob for the specified
// state history. The index contains fixed-size entries that locate storage slot
// 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, offset, length int) ([]byte, error) {
return db.AncientBytes(stateHistoryStorageIndex, id-1, uint64(offset), uint64(length))
}
// ReadStateAccountHistory retrieves the state root corresponding to the specified
// state history. 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 {
blob, err := db.Ancient(stateHistoryAccountData, id-1)
if err != nil {
return nil
}
return blob
// ReadStateAccountHistory retrieves the concatenated account data blob for the
// specified state history. Offsets and lengths are resolved via the account
// 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, offset, length int) ([]byte, error) {
return db.AncientBytes(stateHistoryAccountData, id-1, uint64(offset), uint64(length))
}
// ReadStateStorageHistory retrieves the state root corresponding to the specified
// state history. 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 {
blob, err := db.Ancient(stateHistoryStorageData, id-1)
if err != nil {
return nil
}
return blob
// ReadStateStorageHistory retrieves the concatenated storage slot data blob for
// the specified state history. Locations are resolved via the account and
// 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, offset, length int) ([]byte, error) {
return db.AncientBytes(stateHistoryStorageData, id-1, uint64(offset), uint64(length))
}
// ReadStateHistory retrieves the state history from database with provided id.
@ -292,3 +287,76 @@ func WriteStateHistory(db ethdb.AncientWriter, id uint64, meta []byte, accountIn
})
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},
}
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.
var (
ChainFreezerName = "chain" // the folder name of chain segment ancient store.
MerkleStateFreezerName = "state" // the folder name of state history ancient store.
VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store.
ChainFreezerName = "chain" // the folder name of chain segment ancient store.
MerkleStateFreezerName = "state" // 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.
var freezers = []string{ChainFreezerName, MerkleStateFreezerName, VerkleStateFreezerName}
var freezers = []string{
ChainFreezerName,
MerkleStateFreezerName, VerkleStateFreezerName,
MerkleTrienodeFreezerName, VerkleTrienodeFreezerName,
}
// 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)
}
// 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

@ -403,6 +403,10 @@ func (f *chainFreezer) AncientRange(kind string, start, count, maxBytes uint64)
return f.ancients.AncientRange(kind, start, count, maxBytes)
}
func (f *chainFreezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
return f.ancients.AncientBytes(kind, id, offset, length)
}
func (f *chainFreezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) {
return f.ancients.ModifyAncients(fn)
}

View file

@ -100,6 +100,12 @@ func (db *nofreezedb) AncientRange(kind string, start, max, maxByteSize uint64)
return nil, errNotSupported
}
// AncientBytes retrieves the value segment of the element specified by the id
// and value offsets.
func (db *nofreezedb) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
return nil, errNotSupported
}
// Ancients returns an error as we don't have a backing chain freezer.
func (db *nofreezedb) Ancients() (uint64, error) {
return 0, errNotSupported

View file

@ -76,8 +76,9 @@ type Freezer struct {
// NewFreezer creates a freezer instance for maintaining immutable ordered
// data according to the given parameters.
//
// The 'tables' argument defines the data tables. If the value of a map
// entry is true, snappy compression is disabled for the table.
// The 'tables' argument defines the freezer tables and their configuration.
// Each value is a freezerTableConfig specifying whether snappy compression is
// disabled (noSnappy) and whether the table is prunable (prunable).
func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) {
// Create the initial freezer object
var (
@ -201,6 +202,15 @@ func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]
return nil, errUnknownTable
}
// AncientBytes retrieves the value segment of the element specified by the id
// and value offsets.
func (f *Freezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
if table := f.tables[kind]; table != nil {
return table.RetrieveBytes(id, offset, length)
}
return nil, errUnknownTable
}
// Ancients returns the length of the frozen items.
func (f *Freezer) Ancients() (uint64, error) {
return f.frozen.Load(), nil

View file

@ -412,3 +412,28 @@ func (f *MemoryFreezer) Reset() error {
func (f *MemoryFreezer) AncientDatadir() (string, error) {
return "", nil
}
// AncientBytes retrieves the value segment of the element specified by the id
// and value offsets.
func (f *MemoryFreezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
f.lock.RLock()
defer f.lock.RUnlock()
table := f.tables[kind]
if table == nil {
return nil, errUnknownTable
}
entries, err := table.retrieve(id, 1, 0)
if err != nil {
return nil, err
}
if len(entries) == 0 {
return nil, errOutOfBounds
}
data := entries[0]
if offset > uint64(len(data)) || offset+length > uint64(len(data)) {
return nil, fmt.Errorf("requested range out of bounds: item size %d, offset %d, length %d", len(data), offset, length)
}
return data[offset : offset+length], nil
}

View file

@ -126,6 +126,15 @@ func (f *resettableFreezer) AncientRange(kind string, start, count, maxBytes uin
return f.freezer.AncientRange(kind, start, count, maxBytes)
}
// AncientBytes retrieves the value segment of the element specified by the id
// and value offsets.
func (f *resettableFreezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
f.lock.RLock()
defer f.lock.RUnlock()
return f.freezer.AncientBytes(kind, id, offset, length)
}
// Ancients returns the length of the frozen items.
func (f *resettableFreezer) Ancients() (uint64, error) {
f.lock.RLock()

View file

@ -100,7 +100,7 @@ type freezerTable struct {
// should never be lower than itemOffset.
itemHidden atomic.Uint64
config freezerTableConfig // if true, disables snappy compression. Note: does not work retroactively
config freezerTableConfig // table configuration (compression, prunability). Note: compression flag does not apply retroactively to existing files
readonly bool
maxFileSize uint32 // Max file size for data-files
name string
@ -1107,6 +1107,71 @@ func (t *freezerTable) retrieveItems(start, count, maxBytes uint64) ([]byte, []i
return output, sizes, nil
}
// RetrieveBytes retrieves the value segment of the element specified by the id
// and value offsets.
func (t *freezerTable) RetrieveBytes(item, offset, length uint64) ([]byte, error) {
t.lock.RLock()
defer t.lock.RUnlock()
if t.index == nil || t.head == nil || t.metadata.file == nil {
return nil, errClosed
}
items, hidden := t.items.Load(), t.itemHidden.Load()
if items <= item || hidden > item {
return nil, errOutOfBounds
}
// Retrieves the index entries for the specified ID and its immediate successor
indices, err := t.getIndices(item, 1)
if err != nil {
return nil, err
}
index0, index1 := indices[0], indices[1]
itemStart, itemLimit, fileId := index0.bounds(index1)
itemSize := itemLimit - itemStart
dataFile, exist := t.files[fileId]
if !exist {
return nil, fmt.Errorf("missing data file %d", fileId)
}
// Perform the partial read if no-compression was enabled upon
if t.config.noSnappy {
if offset > uint64(itemSize) || offset+length > uint64(itemSize) {
return nil, fmt.Errorf("requested range out of bounds: item size %d, offset %d, length %d", itemSize, offset, length)
}
itemStart += uint32(offset)
buf := make([]byte, length)
_, err = dataFile.ReadAt(buf, int64(itemStart))
if err != nil {
return nil, err
}
t.readMeter.Mark(int64(length))
return buf, nil
} else {
// If compressed, read the full item, decompress, then slice.
// Unfortunately, in this case, there is no performance gain
// by performing the partial read at all.
buf := make([]byte, itemSize)
_, err = dataFile.ReadAt(buf, int64(itemStart))
if err != nil {
return nil, err
}
t.readMeter.Mark(int64(itemSize))
data, err := snappy.Decode(nil, buf)
if err != nil {
return nil, err
}
if offset > uint64(len(data)) || offset+length > uint64(len(data)) {
return nil, fmt.Errorf("requested range out of bounds: item size %d, offset %d, length %d", len(data), offset, length)
}
return data[offset : offset+length], nil
}
}
// size returns the total data size in the freezer table.
func (t *freezerTable) size() (uint64, error) {
t.lock.RLock()

View file

@ -1571,3 +1571,65 @@ func TestTailTruncationCrash(t *testing.T) {
t.Fatalf("Unexpected index flush offset, want: %d, got: %d", 26*indexEntrySize, f.metadata.flushOffset)
}
}
func TestFreezerAncientBytes(t *testing.T) {
t.Parallel()
types := []struct {
name string
config freezerTableConfig
}{
{"uncompressed", freezerTableConfig{noSnappy: true}},
{"compressed", freezerTableConfig{noSnappy: false}},
}
for _, typ := range types {
t.Run(typ.name, func(t *testing.T) {
f, err := newTable(os.TempDir(), fmt.Sprintf("ancientbytes-%s-%d", typ.name, rand.Uint64()), metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 1000, typ.config, false)
if err != nil {
t.Fatal(err)
}
defer f.Close()
for i := 0; i < 10; i++ {
data := getChunk(100, i)
batch := f.newBatch()
require.NoError(t, batch.AppendRaw(uint64(i), data))
require.NoError(t, batch.commit())
}
for i := 0; i < 10; i++ {
full, err := f.Retrieve(uint64(i))
require.NoError(t, err)
// Full read
got, err := f.RetrieveBytes(uint64(i), 0, uint64(len(full)))
require.NoError(t, err)
if !bytes.Equal(got, full) {
t.Fatalf("full read mismatch for entry %d", i)
}
// Empty read
got, err = f.RetrieveBytes(uint64(i), 0, 0)
require.NoError(t, err)
if !bytes.Equal(got, full[:0]) {
t.Fatalf("empty read mismatch for entry %d", i)
}
// Middle slice
got, err = f.RetrieveBytes(uint64(i), 10, 50)
require.NoError(t, err)
if !bytes.Equal(got, full[10:60]) {
t.Fatalf("middle slice mismatch for entry %d", i)
}
// Single byte
got, err = f.RetrieveBytes(uint64(i), 99, 1)
require.NoError(t, err)
if !bytes.Equal(got, full[99:100]) {
t.Fatalf("single byte mismatch for entry %d", i)
}
// Out of bounds
_, err = f.RetrieveBytes(uint64(i), 100, 1)
if err == nil {
t.Fatalf("expected error for out-of-bounds read for entry %d", i)
}
}
})
}
}

View file

@ -239,7 +239,7 @@ func TestFreezerConcurrentModifyTruncate(t *testing.T) {
// fails, otherwise it succeeds. In either case, the freezer should be positioned
// at 10 after both operations are done.
if truncateErr != nil {
t.Fatal("concurrent truncate failed:", err)
t.Fatal("concurrent truncate failed:", truncateErr)
}
if !(errors.Is(modifyErr, nil) || errors.Is(modifyErr, errOutOrderInsertion)) {
t.Fatal("wrong error from concurrent modify:", modifyErr)

View file

@ -80,6 +80,10 @@ var (
// been indexed.
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 = []byte("TransactionIndexTail")
@ -125,8 +129,10 @@ var (
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account 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
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:
// (a) Trie nodes
@ -395,27 +401,34 @@ func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []
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
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
var buf4 [4]byte
binary.BigEndian.PutUint32(buf4[:], blockID)
totalLen := len(StateHistoryAccountBlockPrefix) + common.HashLength + 4
out := make([]byte, totalLen)
off := 0
off += copy(out[off:], StateHistoryAccountBlockPrefix)
off += copy(out[off:], addressHash.Bytes())
copy(out[off:], buf4[:])
binary.BigEndian.PutUint32(out[off:], blockID)
return out
}
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
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
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:], addressHash.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
}

View file

@ -62,6 +62,12 @@ func (t *table) AncientRange(kind string, start, count, maxBytes uint64) ([][]by
return t.db.AncientRange(kind, start, count, maxBytes)
}
// AncientBytes is a noop passthrough that just forwards the request to the underlying
// database.
func (t *table) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
return t.db.AncientBytes(kind, id, offset, length)
}
// Ancients is a noop passthrough that just forwards the request to the underlying
// database.
func (t *table) Ancients() (uint64, error) {

View file

@ -149,8 +149,7 @@ func BenchmarkHashing(b *testing.B) {
var got common.Hash
var hasher = sha3.NewLegacyKeccak256()
b.Run("iteratorhashing", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
var hash common.Hash
it, err := rlp.NewListIterator(bodyRlp)
if err != nil {
@ -172,8 +171,7 @@ func BenchmarkHashing(b *testing.B) {
})
var exp common.Hash
b.Run("fullbodyhashing", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
var body types.Body
rlp.DecodeBytes(bodyRlp, &body)
for _, tx := range body.Transactions {
@ -182,8 +180,7 @@ func BenchmarkHashing(b *testing.B) {
}
})
b.Run("fullblockhashing", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
var block types.Block
rlp.DecodeBytes(blockRlp, &block)
for _, tx := range block.Transactions() {

View file

@ -131,7 +131,7 @@ func TestMessageCallGas(t *testing.T) {
}
gas = ae.CodeHashGas(testAddr, false, math.MaxUint64, false)
if gas != params.WitnessChunkReadCost {
t.Fatalf("incorrect gas computed, got %d, want %d", gas, 0)
t.Fatalf("incorrect gas computed, got %d, want %d", gas, params.WitnessChunkReadCost)
}
// Check warm read cost

View file

@ -302,6 +302,8 @@ func mustCopyTrie(t Trie) Trie {
return t.Copy()
case *trie.VerkleTrie:
return t.Copy()
case *trie.TransitionTrie:
return t.Copy()
default:
panic(fmt.Errorf("unknown trie type %T", t))
}

View file

@ -229,8 +229,7 @@ func BenchmarkSearch(b *testing.B) {
layer = fill(layer)
}
key := crypto.Keccak256Hash([]byte{0x13, 0x38})
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
layer.AccountRLP(key)
}
}
@ -269,8 +268,7 @@ func BenchmarkSearchSlot(b *testing.B) {
for i := 0; i < 128; i++ {
layer = fill(layer)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
layer.Storage(accountKey, storageKey)
}
}
@ -300,9 +298,7 @@ func BenchmarkFlatten(b *testing.B) {
}
return newDiffLayer(parent, common.Hash{}, accounts, storage)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
for b.Loop() {
var layer snapshot
layer = emptyLayer()
for i := 1; i < 128; i++ {
@ -352,9 +348,7 @@ func BenchmarkJournal(b *testing.B) {
for i := 1; i < 128; i++ {
layer = fill(layer)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
layer.Journal(new(bytes.Buffer))
}
}

View file

@ -928,7 +928,7 @@ func BenchmarkAccountIteratorTraversal(b *testing.B) {
head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
b.Run("binary iterator keys", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := 0
it := head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
for it.Next() {
@ -940,7 +940,7 @@ func BenchmarkAccountIteratorTraversal(b *testing.B) {
}
})
b.Run("binary iterator values", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := 0
it := head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
for it.Next() {
@ -953,7 +953,7 @@ func BenchmarkAccountIteratorTraversal(b *testing.B) {
}
})
b.Run("fast iterator keys", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
it, _ := snaps.AccountIterator(common.HexToHash("0x65"), common.Hash{})
defer it.Release()
@ -967,7 +967,7 @@ func BenchmarkAccountIteratorTraversal(b *testing.B) {
}
})
b.Run("fast iterator values", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
it, _ := snaps.AccountIterator(common.HexToHash("0x65"), common.Hash{})
defer it.Release()
@ -1025,7 +1025,7 @@ func BenchmarkAccountIteratorLargeBaselayer(b *testing.B) {
head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
b.Run("binary iterator (keys)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := 0
it := head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
for it.Next() {
@ -1037,7 +1037,7 @@ func BenchmarkAccountIteratorLargeBaselayer(b *testing.B) {
}
})
b.Run("binary iterator (values)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := 0
it := head.(*diffLayer).newBinaryAccountIterator(common.Hash{})
for it.Next() {
@ -1051,7 +1051,7 @@ func BenchmarkAccountIteratorLargeBaselayer(b *testing.B) {
}
})
b.Run("fast iterator (keys)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
it, _ := snaps.AccountIterator(common.HexToHash("0x65"), common.Hash{})
defer it.Release()
@ -1065,7 +1065,7 @@ func BenchmarkAccountIteratorLargeBaselayer(b *testing.B) {
}
})
b.Run("fast iterator (values)", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
it, _ := snaps.AccountIterator(common.HexToHash("0x65"), common.Hash{})
defer it.Release()

View file

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/holiman/uint256"
)
@ -333,7 +334,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
continue
}
if !exist {
log.Error("Storage slot is not found in pending area", s.address, "slot", key)
log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key)
continue
}
if (value != common.Hash{}) {
@ -494,8 +495,20 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject {
selfDestructed: s.selfDestructed,
newContract: s.newContract,
}
if s.trie != nil {
switch s.trie.(type) {
case *trie.VerkleTrie:
// Verkle uses only one tree, and the copy has already been
// made in mustCopyTrie.
obj.trie = db.trie
case *trie.TransitionTrie:
// Same thing for the transition tree, since the MPT is
// read-only.
obj.trie = db.trie
case *trie.StateTrie:
obj.trie = mustCopyTrie(s.trie)
case nil:
// do nothing
}
return obj
}

View file

@ -25,7 +25,7 @@ import (
func BenchmarkCutOriginal(b *testing.B) {
value := common.HexToHash("0x01")
for i := 0; i < b.N; i++ {
for b.Loop() {
bytes.TrimLeft(value[:], "\x00")
}
}
@ -33,14 +33,14 @@ func BenchmarkCutOriginal(b *testing.B) {
func BenchmarkCutsetterFn(b *testing.B) {
value := common.HexToHash("0x01")
cutSetFn := func(r rune) bool { return r == 0 }
for i := 0; i < b.N; i++ {
for b.Loop() {
bytes.TrimLeftFunc(value[:], cutSetFn)
}
}
func BenchmarkCutCustomTrim(b *testing.B) {
value := common.HexToHash("0x01")
for i := 0; i < b.N; i++ {
for b.Loop() {
common.TrimLeftZeroes(value[:])
}
}

View file

@ -94,6 +94,14 @@ func TestSizeTracker(t *testing.T) {
}
baselineRoot := currentRoot
// Close and reopen the trie database so all async flushes triggered by the
// baseline commits are written before we measure the baseline snapshot.
if err := tdb.Close(); err != nil {
t.Fatalf("Failed to close triedb before baseline measurement: %v", err)
}
tdb = triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults})
sdb = NewDatabase(tdb, nil)
// Wait for snapshot completion
for !tdb.SnapshotCompleted() {
time.Sleep(100 * time.Millisecond)
@ -215,13 +223,12 @@ func TestSizeTracker(t *testing.T) {
if actualStats.ContractCodeBytes != expectedStats.ContractCodeBytes {
t.Errorf("Contract code bytes mismatch: expected %d, got %d", expectedStats.ContractCodeBytes, actualStats.ContractCodeBytes)
}
// TODO: failed on github actions, need to investigate
// if actualStats.AccountTrienodes != expectedStats.AccountTrienodes {
// t.Errorf("Account trie nodes mismatch: expected %d, got %d", expectedStats.AccountTrienodes, actualStats.AccountTrienodes)
// }
// if actualStats.AccountTrienodeBytes != expectedStats.AccountTrienodeBytes {
// t.Errorf("Account trie node bytes mismatch: expected %d, got %d", expectedStats.AccountTrienodeBytes, actualStats.AccountTrienodeBytes)
// }
if actualStats.AccountTrienodes != expectedStats.AccountTrienodes {
t.Errorf("Account trie nodes mismatch: expected %d, got %d", expectedStats.AccountTrienodes, actualStats.AccountTrienodes)
}
if actualStats.AccountTrienodeBytes != expectedStats.AccountTrienodeBytes {
t.Errorf("Account trie node bytes mismatch: expected %d, got %d", expectedStats.AccountTrienodeBytes, actualStats.AccountTrienodeBytes)
}
if actualStats.StorageTrienodes != expectedStats.StorageTrienodes {
t.Errorf("Storage trie nodes mismatch: expected %d, got %d", expectedStats.StorageTrienodes, actualStats.StorageTrienodes)
}

View file

@ -191,17 +191,18 @@ func (s *hookedStateDB) SetNonce(address common.Address, nonce uint64, reason tr
func (s *hookedStateDB) SetCode(address common.Address, code []byte, reason tracing.CodeChangeReason) []byte {
prev := s.inner.SetCode(address, code, reason)
if s.hooks.OnCodeChangeV2 != nil || s.hooks.OnCodeChange != nil {
prevHash := types.EmptyCodeHash
if len(prev) != 0 {
prevHash = crypto.Keccak256Hash(prev)
}
prevHash := crypto.Keccak256Hash(prev)
codeHash := crypto.Keccak256Hash(code)
if s.hooks.OnCodeChangeV2 != nil {
s.hooks.OnCodeChangeV2(address, prevHash, prev, codeHash, code, reason)
} else if s.hooks.OnCodeChange != nil {
s.hooks.OnCodeChange(address, prevHash, prev, codeHash, code)
// Invoke the hooks only if the contract code is changed
if prevHash != codeHash {
if s.hooks.OnCodeChangeV2 != nil {
s.hooks.OnCodeChangeV2(address, prevHash, prev, codeHash, code, reason)
} else if s.hooks.OnCodeChange != nil {
s.hooks.OnCodeChange(address, prevHash, prev, codeHash, code)
}
}
}
return prev

View file

@ -772,7 +772,7 @@ func TestCopyCommitCopy(t *testing.T) {
t.Fatalf("second copy non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyTwo.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("second copy committed storage slot mismatch: have %x, want %x", val, sval)
t.Fatalf("second copy committed storage slot mismatch: have %x, want %x", val, common.Hash{})
}
// Commit state, ensure states can be loaded from disk
root, _ := state.Commit(0, false, false)
@ -859,7 +859,7 @@ func TestCopyCopyCommitCopy(t *testing.T) {
t.Fatalf("third copy non-committed storage slot mismatch: have %x, want %x", val, sval)
}
if val := copyThree.GetCommittedState(addr, skey); val != (common.Hash{}) {
t.Fatalf("third copy committed storage slot mismatch: have %x, want %x", val, sval)
t.Fatalf("third copy committed storage slot mismatch: have %x, want %x", val, common.Hash{})
}
}
@ -912,10 +912,10 @@ func TestCommitCopy(t *testing.T) {
}
// Slots cached in the stateDB, available after commit
if val := copied.GetState(addr, skey2); val != sval2 {
t.Fatalf("unexpected storage slot: have %x", sval1)
t.Fatalf("unexpected storage slot: have %x, want %x", val, sval2)
}
if val := copied.GetCommittedState(addr, skey2); val != sval2 {
t.Fatalf("unexpected storage slot: have %x", val)
t.Fatalf("unexpected storage slot: have %x, want %x", val, sval2)
}
}

View file

@ -35,18 +35,21 @@ import (
//
// StateProcessor implements Processor.
type StateProcessor struct {
config *params.ChainConfig // Chain configuration options
chain *HeaderChain // Canonical header chain
chain ChainContext // Chain context interface
}
// NewStateProcessor initialises a new StateProcessor.
func NewStateProcessor(config *params.ChainConfig, chain *HeaderChain) *StateProcessor {
func NewStateProcessor(chain ChainContext) *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
// the transaction messages using the statedb and applying any rewards to both
// 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.
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) {
var (
config = p.chainConfig()
receipts types.Receipts
usedGas = new(uint64)
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
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)
}
var (
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.
@ -80,12 +84,12 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
tracingStateDB = state.NewHookedState(statedb, hooks)
}
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 {
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)
}
@ -106,10 +110,10 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
}
// Read requests if Prague is enabled.
var requests [][]byte
if p.config.IsPrague(block.Number(), block.Time()) {
if config.IsPrague(block.Number(), block.Time()) {
requests = [][]byte{}
// 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)
}
// 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)
p.chain.engine.Finalize(p.chain, header, tracingStateDB, block.Body())
p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body())
return &ProcessResult{
Receipts: receipts,

View file

@ -164,8 +164,12 @@ type Message struct {
// or the state prefetching.
SkipNonceChecks bool
// When SkipFromEOACheck is true, the message sender is not checked to be an EOA.
SkipFromEOACheck bool
// When set, the message is not treated as a transaction, and certain
// transaction-specific checks are skipped:
//
// - From is not verified to be an EOA
// - GasLimit is not checked against the protocol defined tx gaslimit
SkipTransactionChecks bool
}
// TransactionToMessage converts a transaction into a Message.
@ -182,7 +186,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
AccessList: tx.AccessList(),
SetCodeAuthorizations: tx.SetCodeAuthorizations(),
SkipNonceChecks: false,
SkipFromEOACheck: false,
SkipTransactionChecks: false,
BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(),
}
@ -320,7 +324,12 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce)
}
}
if !msg.SkipFromEOACheck {
isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap.
if isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
// Make sure the sender is an EOA
code := st.state.GetCode(msg.From)
_, delegated := types.ParseDelegation(code)
@ -354,7 +363,6 @@ func (st *stateTransition) preCheck() error {
}
}
// Check the blob version validity
isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
if msg.BlobHashes != nil {
// The to field of a blob tx type is mandatory, and a `BlobTx` transaction internally
// has it as a non-nillable value, so any msg derived from blob transaction has it non-nil.
@ -398,10 +406,6 @@ func (st *stateTransition) preCheck() error {
return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From)
}
}
// Verify tx gas limit does not exceed EIP-7825 cap.
if isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
return st.buyGas()
}

View file

@ -62,7 +62,7 @@ func ExecuteStateless(config *params.ChainConfig, vmconfig vm.Config, block *typ
headerCache: lru.NewCache[common.Hash, *types.Header](256),
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
// Run the stateless blocks processing and self-validate certain fields

View file

@ -21,12 +21,15 @@ import (
"container/heap"
"errors"
"fmt"
"maps"
"math"
"math/big"
"os"
"path/filepath"
"slices"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
@ -61,11 +64,11 @@ const (
// small buffer is added to the proof overhead.
txBlobOverhead = uint32(kzg4844.CellProofsPerBlob*len(kzg4844.Proof{}) + 64)
// txMaxSize is the maximum size a single transaction can have, outside
// the included blobs. Since blob transactions are pulled instead of pushed,
// and only a small metadata is kept in ram, the rest is on disk, there is
// no critical limit that should be enforced. Still, capping it to some sane
// limit can never hurt.
// txMaxSize is the maximum size a single transaction can have, including the
// blobs. Since blob transactions are pulled instead of pushed, and only a
// small metadata is kept in ram, the rest is on disk, there is no critical
// limit that should be enforced. Still, capping it to some sane limit can
// never hurt, which is aligned with maxBlobsPerTx constraint enforced internally.
txMaxSize = 1024 * 1024
// maxBlobsPerTx is the maximum number of blobs that a single transaction can
@ -93,6 +96,11 @@ const (
// storeVersion is the current slotter layout used for the billy.Database
// store.
storeVersion = 1
// conversionTimeWindow defines the period after the Osaka fork during which
// the pool will still accept and convert legacy blob transactions. After this
// window, all legacy blob transactions will be rejected.
conversionTimeWindow = time.Hour * 2
)
// blobTxMeta is the minimal subset of types.BlobTx necessary to validate and
@ -396,12 +404,13 @@ type BlobPool struct {
stored uint64 // Useful data size of all transactions on disk
limbo *limbo // Persistent data store for the non-finalized blobs
signer types.Signer // Transaction signer to use for sender recovery
chain BlockChain // Chain object to access the state through
signer types.Signer // Transaction signer to use for sender recovery
chain BlockChain // Chain object to access the state through
cQueue *conversionQueue // The queue for performing legacy sidecar conversion (TODO: remove after Osaka)
head *types.Header // Current head of the chain
state *state.StateDB // Current state at the head of the chain
gasTip *uint256.Int // Currently accepted minimum gas tip
head atomic.Pointer[types.Header] // Current head of the chain
state *state.StateDB // Current state at the head of the chain
gasTip atomic.Pointer[uint256.Int] // Currently accepted minimum gas tip
lookup *lookup // Lookup table mapping blobs to txs and txs to billy entries
index map[common.Address][]*blobTxMeta // Blob transactions grouped by accounts, sorted by nonce
@ -426,6 +435,7 @@ func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bo
hasPendingAuth: hasPendingAuth,
signer: types.LatestSigner(chain.Config()),
chain: chain,
cQueue: newConversionQueue(), // Deprecate it after the osaka fork
lookup: newLookup(),
index: make(map[common.Address][]*blobTxMeta),
spent: make(map[common.Address]*uint256.Int),
@ -467,7 +477,8 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
if err != nil {
return err
}
p.head, p.state = head, state
p.head.Store(head)
p.state = state
// Create new slotter for pre-Osaka blob configuration.
slotter := newSlotter(eip4844.LatestMaxBlobsPerBlock(p.chain.Config()))
@ -545,11 +556,11 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
p.recheck(addr, nil)
}
var (
basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), p.head))
basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), head))
blobfee = uint256.NewInt(params.BlobTxMinBlobGasprice)
)
if p.head.ExcessBlobGas != nil {
blobfee = uint256.MustFromBig(eip4844.CalcBlobFee(p.chain.Config(), p.head))
if head.ExcessBlobGas != nil {
blobfee = uint256.MustFromBig(eip4844.CalcBlobFee(p.chain.Config(), head))
}
p.evict = newPriceHeap(basefee, blobfee, p.index)
@ -579,6 +590,9 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
// Close closes down the underlying persistent store.
func (p *BlobPool) Close() error {
// Terminate the conversion queue
p.cQueue.close()
var errs []error
if p.limbo != nil { // Close might be invoked due to error in constructor, before p,limbo is set
if err := p.limbo.Close(); err != nil {
@ -952,7 +966,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
log.Error("Failed to reset blobpool state", "err", err)
return
}
p.head = newHead
p.head.Store(newHead)
p.state = statedb
// Run the reorg between the old and new head and figure out which accounts
@ -975,7 +989,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
}
}
// Flush out any blobs from limbo that are older than the latest finality
if p.chain.Config().IsCancun(p.head.Number, p.head.Time) {
if p.chain.Config().IsCancun(newHead.Number, newHead.Time) {
p.limbo.finalize(p.chain.CurrentFinalBlock())
}
// Reset the price heap for the new set of basefee/blobfee pairs
@ -991,6 +1005,172 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
basefeeGauge.Update(int64(basefee.Uint64()))
blobfeeGauge.Update(int64(blobfee.Uint64()))
p.updateStorageMetrics()
// Perform the conversion logic at the fork boundary
if !p.chain.Config().IsOsaka(oldHead.Number, oldHead.Time) && p.chain.Config().IsOsaka(newHead.Number, newHead.Time) {
// Deep copy all indexed transaction metadata.
var (
ids = make(map[common.Address]map[uint64]uint64)
txs = make(map[common.Address]map[uint64]common.Hash)
)
for sender, list := range p.index {
ids[sender] = make(map[uint64]uint64)
txs[sender] = make(map[uint64]common.Hash)
for _, m := range list {
ids[sender][m.nonce] = m.id
txs[sender][m.nonce] = m.hash
}
}
// Initiate the background conversion thread.
p.cQueue.launchBillyConversion(func() {
p.convertLegacySidecars(ids, txs)
})
}
}
// compareAndSwap checks if the specified transaction is still tracked in the pool
// and replace the metadata accordingly. It should only be used in the fork boundary
// bulk conversion. If it fails for some reason, the subsequent txs won't be dropped
// for simplicity which we assume it's very likely to happen.
//
// The returned flag indicates whether the replacement succeeded.
func (p *BlobPool) compareAndSwap(address common.Address, hash common.Hash, blob []byte, oldID uint64, oldStorageSize uint32) bool {
p.lock.Lock()
defer p.lock.Unlock()
newId, err := p.store.Put(blob)
if err != nil {
log.Error("Failed to store transaction", "hash", hash, "err", err)
return false
}
newSize := uint64(len(blob))
newStorageSize := p.store.Size(newId)
// Terminate the procedure if the transaction was already evicted. The
// newly added blob should be removed before return.
if !p.lookup.update(hash, newId, newSize) {
if derr := p.store.Delete(newId); derr != nil {
log.Error("Failed to delete the dangling blob tx", "err", derr)
} else {
log.Warn("Deleted the dangling blob tx", "id", newId)
}
return false
}
// Update the metadata of blob transaction
for _, meta := range p.index[address] {
if meta.hash == hash {
meta.id = newId
meta.version = types.BlobSidecarVersion1
meta.storageSize = newStorageSize
meta.size = newSize
p.stored += uint64(newStorageSize)
p.stored -= uint64(oldStorageSize)
break
}
}
if err := p.store.Delete(oldID); err != nil {
log.Error("Failed to delete the legacy transaction", "hash", hash, "id", oldID, "err", err)
}
return true
}
// convertLegacySidecar fetches transaction data from the store, performs an
// on-the-fly conversion. This function is intended for use only during the
// Osaka fork transition period.
//
// The returned flag indicates whether the replacement succeeds or not.
func (p *BlobPool) convertLegacySidecar(sender common.Address, hash common.Hash, id uint64) bool {
start := time.Now()
// Retrieves the legacy blob transaction from the underlying store with
// read lock held, preventing any potential data race around the slot
// specified by the id.
p.lock.RLock()
data, err := p.store.Get(id)
if err != nil {
p.lock.RUnlock()
// The transaction may have been evicted simultaneously, safe to skip conversion.
log.Debug("Blob transaction is missing", "hash", hash, "id", id, "err", err)
return false
}
oldStorageSize := p.store.Size(id)
p.lock.RUnlock()
// Decode the transaction, the failure is not expected and report the error
// loudly if possible. If the blob transaction in this slot is corrupted,
// leave it in the store, it will be dropped during the next pool
// initialization.
var tx types.Transaction
if err = rlp.DecodeBytes(data, &tx); err != nil {
log.Error("Blob transaction is corrupted", "hash", hash, "id", id, "err", err)
return false
}
// Skip conversion if the transaction does not match the expected hash, or if it was
// already converted. This can occur if the original transaction was evicted from the
// pool and the slot was reused by a new one.
if tx.Hash() != hash {
log.Warn("Blob transaction was replaced", "hash", hash, "id", id, "stored", tx.Hash())
return false
}
sc := tx.BlobTxSidecar()
if sc.Version >= types.BlobSidecarVersion1 {
log.Debug("Skipping conversion of blob tx", "hash", hash, "id", id)
return false
}
// Perform the sidecar conversion, the failure is not expected and report the error
// loudly if possible.
if err := tx.BlobTxSidecar().ToV1(); err != nil {
log.Error("Failed to convert blob transaction", "hash", hash, "err", err)
return false
}
// Encode the converted transaction, the failure is not expected and report
// the error loudly if possible.
blob, err := rlp.EncodeToBytes(&tx)
if err != nil {
log.Error("Failed to encode blob transaction", "hash", tx.Hash(), "err", err)
return false
}
// Replace the legacy blob transaction with the converted format.
if !p.compareAndSwap(sender, hash, blob, id, oldStorageSize) {
log.Error("Failed to replace the legacy transaction", "hash", hash)
return false
}
log.Debug("Converted legacy blob transaction", "hash", hash, "elapsed", common.PrettyDuration(time.Since(start)))
return true
}
// convertLegacySidecars converts all given transactions to sidecar version 1.
//
// If any of them fails to be converted, the subsequent transactions will still
// be processed, as we assume the failure is very unlikely to happen. If happens,
// these transactions will be stuck in the pool until eviction.
func (p *BlobPool) convertLegacySidecars(ids map[common.Address]map[uint64]uint64, txs map[common.Address]map[uint64]common.Hash) {
var (
start = time.Now()
success int
failure int
)
for addr, list := range txs {
// Transactions evicted from the pool must be contiguous, if in any case,
// the transactions are gapped with each other, they will be discarded.
nonces := slices.Collect(maps.Keys(list))
slices.Sort(nonces)
// Convert the txs with nonce order
for _, nonce := range nonces {
if p.convertLegacySidecar(addr, list[nonce], ids[addr][nonce]) {
success++
} else {
failure++
}
}
}
log.Info("Completed blob transaction conversion", "discarded", failure, "injected", success, "elapsed", common.PrettyDuration(time.Since(start)))
}
// reorg assembles all the transactors and missing transactions between an old
@ -1130,7 +1310,7 @@ func (p *BlobPool) reorg(oldHead, newHead *types.Header) (map[common.Address][]*
func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
// Retrieve the associated blob from the limbo. Without the blobs, we cannot
// add the transaction back into the pool as it is not mineable.
tx, err := p.limbo.pull(txhash)
pooledTx, err := p.limbo.pull(txhash)
if err != nil {
log.Error("Blobs unavailable, dropping reorged tx", "err", err)
return err
@ -1138,23 +1318,43 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
// TODO: seems like an easy optimization here would be getting the serialized tx
// from limbo instead of re-serializing it here.
// Converts reorged-out legacy blob transactions to the new format to prevent
// them from becoming stuck in the pool until eviction.
//
// Performance note: Conversion takes ~140ms (Mac M1 Pro). Since a maximum of
// 9 legacy blob transactions are allowed in a block pre-Osaka, an adversary
// could theoretically halt a Geth node for ~1.2s by reorging per block. However,
// this attack is financially inefficient to execute.
head := p.head.Load()
if p.chain.Config().IsOsaka(head.Number, head.Time) && pooledTx.Sidecar.Version == types.BlobSidecarVersion0 {
tx, err := pooledTx.convert()
if err != nil {
log.Error("Failed to convert the pooledTx", "err", err)
return err
}
if err := tx.BlobTxSidecar().ToV1(); err != nil {
log.Error("Failed to convert the legacy sidecar", "err", err)
return err
}
log.Info("Legacy blob transaction is reorged", "hash", tx.Hash())
}
// Serialize the transaction back into the primary datastore.
blob, err := rlp.EncodeToBytes(tx)
blob, err := rlp.EncodeToBytes(pooledTx)
if err != nil {
log.Error("Failed to encode transaction for storage", "hash", tx.Transaction.Hash(), "err", err)
log.Error("Failed to encode transaction for storage", "hash", pooledTx.Transaction.Hash(), "err", err)
return err
}
id, err := p.store.Put(blob)
if err != nil {
log.Error("Failed to write transaction into storage", "hash", tx.Transaction.Hash(), "err", err)
log.Error("Failed to write transaction into storage", "hash", pooledTx.Transaction.Hash(), "err", err)
return err
}
// Update the indices and metrics
meta := newBlobTxMeta(id, tx.Size, p.store.Size(id), tx)
meta := newBlobTxMeta(id, pooledTx.Size, p.store.Size(id), pooledTx)
if _, ok := p.index[addr]; !ok {
if err := p.reserver.Hold(addr); err != nil {
log.Warn("Failed to reserve account for blob pool", "tx", tx.Transaction.Hash(), "from", addr, "err", err)
log.Warn("Failed to reserve account for blob pool", "tx", pooledTx.Transaction.Hash(), "from", addr, "err", err)
return err
}
p.index[addr] = []*blobTxMeta{meta}
@ -1176,14 +1376,15 @@ func (p *BlobPool) SetGasTip(tip *big.Int) {
defer p.lock.Unlock()
// Store the new minimum gas tip
old := p.gasTip
p.gasTip = uint256.MustFromBig(tip)
old := p.gasTip.Load()
newTip := uint256.MustFromBig(tip)
p.gasTip.Store(newTip)
// If the min miner fee increased, remove transactions below the new threshold
if old == nil || p.gasTip.Cmp(old) > 0 {
if old == nil || newTip.Cmp(old) > 0 {
for addr, txs := range p.index {
for i, tx := range txs {
if tx.execTipCap.Cmp(p.gasTip) < 0 {
if tx.execTipCap.Cmp(newTip) < 0 {
// Drop the offending transaction
var (
ids = []uint64{tx.id}
@ -1243,10 +1444,10 @@ func (p *BlobPool) ValidateTxBasics(tx *types.Transaction) error {
Config: p.chain.Config(),
Accept: 1 << types.BlobTxType,
MaxSize: txMaxSize,
MinTip: p.gasTip.ToBig(),
MinTip: p.gasTip.Load().ToBig(),
MaxBlobCount: maxBlobsPerTx,
}
return txpool.ValidateTransaction(tx, p.head, p.signer, opts)
return txpool.ValidateTransaction(tx, p.head.Load(), p.signer, opts)
}
// checkDelegationLimit determines if the tx sender is delegated or has a
@ -1284,8 +1485,11 @@ func (p *BlobPool) checkDelegationLimit(tx *types.Transaction) error {
// validateTx checks whether a transaction is valid according to the consensus
// rules and adheres to some heuristic limits of the local node (price and size).
//
// This function assumes the static validation has been performed already and
// only runs the stateful checks with lock protection.
func (p *BlobPool) validateTx(pooledTx *pooledBlobTx) error {
if err := txpool.ValidateBlobSidecar(pooledTx.Transaction, pooledTx.Sidecar, p.head, &txpool.ValidationOptions{
if err := txpool.ValidateBlobSidecar(pooledTx.Transaction, pooledTx.Sidecar, &txpool.ValidationOptions{
Config: p.chain.Config(),
MaxBlobCount: maxBlobsPerTx,
}); err != nil {
@ -1293,9 +1497,6 @@ func (p *BlobPool) validateTx(pooledTx *pooledBlobTx) error {
}
tx := pooledTx.Transaction
if err := p.ValidateTxBasics(tx); err != nil {
return err
}
// Ensure the transaction adheres to the stateful pool filters (nonce, balance)
stateOpts := &txpool.ValidationOptionsWithState{
State: p.state,
@ -1482,7 +1683,11 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
//
// This is a utility method for the engine API, enabling consensus clients to
// retrieve blobs from the pools directly instead of the network.
func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blob, []kzg4844.Commitment, [][]kzg4844.Proof, error) {
//
// The version argument specifies the type of proofs to return, either the
// blob proofs (version 0) or the cell proofs (version 1). Proofs conversion is
// CPU intensive, so only done if explicitly requested with the convert flag.
func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte, convert bool) ([]*kzg4844.Blob, []kzg4844.Commitment, [][]kzg4844.Proof, error) {
var (
blobs = make([]*kzg4844.Blob, len(vhashes))
commitments = make([]kzg4844.Commitment, len(vhashes))
@ -1494,13 +1699,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
for i, h := range vhashes {
indices[h] = append(indices[h], i)
}
for _, vhash := range vhashes {
// Skip duplicate vhash that was already resolved in a previous iteration
if _, ok := filled[vhash]; ok {
// Skip vhash that was already resolved in a previous iteration
continue
}
// Retrieve the corresponding blob tx with the vhash, skip blob resolution
// if it's not found locally and place the null instead.
// Retrieve the corresponding blob tx with the vhash.
p.lock.RLock()
txID, exists := p.lookup.storeidOfBlob(vhash)
p.lock.RUnlock()
@ -1536,6 +1742,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
if !ok {
continue // non-interesting blob
}
// Mark hash as seen.
filled[hash] = struct{}{}
if sidecar.Version != version && !convert {
// Skip blobs with incompatible version. Note we still track the blob hash
// in `filled` here, ensuring that we do not resolve this tx another time.
continue
}
// Get or convert the proof.
var pf []kzg4844.Proof
switch version {
case types.BlobSidecarVersion0:
@ -1568,7 +1782,6 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
commitments[index] = sidecar.Commitments[i]
proofs[index] = pf
}
filled[hash] = struct{}{}
}
}
return blobs, commitments, proofs, nil
@ -1589,23 +1802,60 @@ func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int {
return available
}
// preCheck performs the static validation upon the provided tx and converts
// the legacy sidecars if Osaka fork has been activated with a short time window.
//
// This function is pure static and lock free.
func (p *BlobPool) preCheck(tx *types.Transaction) error {
var (
head = p.head.Load()
isOsaka = p.chain.Config().IsOsaka(head.Number, head.Time)
deadline time.Time
)
if isOsaka {
deadline = time.Unix(int64(*p.chain.Config().OsakaTime), 0).Add(conversionTimeWindow)
}
// Validate the transaction statically at first to avoid unnecessary
// conversion. This step doesn't require lock protection.
if err := p.ValidateTxBasics(tx); err != nil {
return err
}
// Before the Osaka fork, reject the blob txs with cell proofs
if !isOsaka {
if tx.BlobTxSidecar().Version == types.BlobSidecarVersion0 {
return nil
} else {
return errors.New("cell proof is not supported yet")
}
}
// After the Osaka fork, reject the legacy blob txs if the conversion
// time window is passed.
if tx.BlobTxSidecar().Version == types.BlobSidecarVersion1 {
return nil
}
if head.Time > uint64(deadline.Unix()) {
return errors.New("legacy blob tx is not supported")
}
// Convert the legacy sidecar after Osaka fork. This could be a long
// procedure which takes a few seconds, even minutes if there is a long
// queue. Fortunately it will only block the routine of the source peer
// announcing the tx, without affecting other parts.
return p.cQueue.convert(tx)
}
// Add inserts a set of blob transactions into the pool if they pass validation (both
// consensus validity and pool restrictions).
//
// Note, if sync is set the method will block until all internal maintenance
// related to the add is finished. Only use this during tests for determinism.
func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error {
var (
errs = make([]error, len(txs))
adds = make([]*types.Transaction, 0, len(txs))
errs []error = make([]error, len(txs))
adds = make([]*types.Transaction, 0, len(txs))
)
for i, tx := range txs {
if errs[i] != nil {
if errs[i] = p.preCheck(tx); errs[i] != nil {
continue
}
pooledTx, err := newPooledBlobTx(tx)
if err != nil {
errs[i] = err
continue
}
errs[i] = p.add(pooledTx)
@ -2106,7 +2356,7 @@ func (p *BlobPool) Clear() {
p.spent = make(map[common.Address]*uint256.Int)
var (
basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), p.head))
basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), p.head.Load()))
blobfee = uint256.NewInt(params.BlobTxMinBlobGasprice)
)
p.evict = newPriceHeap(basefee, blobfee, p.index)

View file

@ -88,6 +88,12 @@ type testBlockChain struct {
statedb *state.StateDB
blocks map[uint64]*types.Block
blockTime *uint64
}
func (bc *testBlockChain) setHeadTime(time uint64) {
bc.blockTime = &time
}
func (bc *testBlockChain) Config() *params.ChainConfig {
@ -105,6 +111,10 @@ func (bc *testBlockChain) CurrentBlock() *types.Header {
blockTime = *bc.config.CancunTime + 1
gasLimit = uint64(30_000_000)
)
if bc.blockTime != nil {
blockTime = *bc.blockTime
}
lo := new(big.Int)
hi := new(big.Int).Mul(big.NewInt(5714), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
@ -423,11 +433,11 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
hashes = append(hashes, tx.vhashes...)
}
}
blobs1, _, proofs1, err := pool.GetBlobs(hashes, types.BlobSidecarVersion0)
blobs1, _, proofs1, err := pool.GetBlobs(hashes, types.BlobSidecarVersion0, false)
if err != nil {
t.Fatal(err)
}
blobs2, _, proofs2, err := pool.GetBlobs(hashes, types.BlobSidecarVersion1)
blobs2, _, proofs2, err := pool.GetBlobs(hashes, types.BlobSidecarVersion1, false)
if err != nil {
t.Fatal(err)
}
@ -441,26 +451,18 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
return
}
for i, hash := range hashes {
// If an item is missing, but shouldn't, error
if blobs1[i] == nil || proofs1[i] == nil {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", i, hash)
continue
}
if blobs2[i] == nil || proofs2[i] == nil {
// If an item is missing from both, but shouldn't, error
if (blobs1[i] == nil || proofs1[i] == nil) && (blobs2[i] == nil || proofs2[i] == nil) {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", i, hash)
continue
}
// Item retrieved, make sure it matches the expectation
index := testBlobIndices[hash]
if *blobs1[i] != *testBlobs[index] {
t.Errorf("retrieved blob mismatch: item %d, hash %x", i, hash)
if blobs1[i] != nil && (*blobs1[i] != *testBlobs[index] || proofs1[i][0] != testBlobProofs[index]) {
t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
continue
}
if proofs1[i][0] != testBlobProofs[index] {
t.Errorf("retrieved proof mismatch: item %d, hash %x", i, hash)
continue
}
if *blobs2[i] != *testBlobs[index] || !slices.Equal(proofs2[i], testBlobCellProofs[index]) {
if blobs2[i] != nil && (*blobs2[i] != *testBlobs[index] || !slices.Equal(proofs2[i], testBlobCellProofs[index])) {
t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
continue
}
@ -1762,10 +1764,8 @@ func TestAdd(t *testing.T) {
// Add each transaction one by one, verifying the pool internals in between
for j, add := range tt.adds {
signed, _ := types.SignNewTx(keys[add.from], types.LatestSigner(params.MainnetChainConfig), add.tx)
pooledTx, _ := newPooledBlobTx(signed)
if err := pool.add(pooledTx); !errors.Is(err, add.err) {
t.Errorf("test %d, tx %d: adding transaction error mismatch: have %v, want %v", i, j, err, add.err)
if errs := pool.Add([]*types.Transaction{signed}, true); !errors.Is(errs[0], add.err) {
t.Errorf("test %d, tx %d: adding transaction error mismatch: have %v, want %v", i, j, errs[0], add.err)
}
if add.err == nil {
size, exist := pool.lookup.sizeOfTx(signed.Hash())
@ -1812,8 +1812,14 @@ func TestAdd(t *testing.T) {
}
}
// Tests adding transactions with legacy sidecars are correctly rejected.
// Tests that transactions with legacy sidecars are accepted within the
// conversion window but rejected after it has passed.
func TestAddLegacyBlobTx(t *testing.T) {
testAddLegacyBlobTx(t, true) // conversion window has not yet passed
testAddLegacyBlobTx(t, false) // conversion window passed
}
func testAddLegacyBlobTx(t *testing.T, accept bool) {
var (
key1, _ = crypto.GenerateKey()
key2, _ = crypto.GenerateKey()
@ -1833,6 +1839,15 @@ func TestAddLegacyBlobTx(t *testing.T) {
blobfee: uint256.NewInt(105),
statedb: statedb,
}
var timeDiff uint64
if accept {
timeDiff = uint64(conversionTimeWindow.Seconds()) - 1
} else {
timeDiff = uint64(conversionTimeWindow.Seconds()) + 1
}
time := *params.MergedTestChainConfig.OsakaTime + timeDiff
chain.setHeadTime(time)
pool := New(Config{Datadir: t.TempDir()}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
t.Fatalf("failed to create blob pool: %v", err)
@ -1842,12 +1857,15 @@ func TestAddLegacyBlobTx(t *testing.T) {
var (
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0)
tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion0)
tx3 = makeMultiBlobTx(1, 1, 800, 70, 6, 12, key2, types.BlobSidecarVersion1)
txs = []*types.Transaction{tx1, tx2}
)
errs := pool.Add([]*types.Transaction{tx1, tx2, tx3}, true)
errs := pool.Add(txs, true)
for _, err := range errs {
if err == nil {
t.Fatalf("expected tx add to fail")
if accept && err != nil {
t.Fatalf("expected tx add to succeed, %v", err)
}
if !accept && err == nil {
t.Fatal("expected tx add to fail")
}
}
verifyPoolInternals(t, pool)
@ -1938,8 +1956,9 @@ func TestGetBlobs(t *testing.T) {
cases := []struct {
start int
limit int
fillRandom bool
version byte
fillRandom bool // Whether to randomly fill some of the requested blobs with unknowns
version byte // Blob sidecar version to request
convert bool // Whether to convert version on retrieval
}{
{
start: 0, limit: 6,
@ -2005,6 +2024,11 @@ func TestGetBlobs(t *testing.T) {
start: 0, limit: 18, fillRandom: true,
version: types.BlobSidecarVersion1,
},
{
start: 0, limit: 18, fillRandom: true,
version: types.BlobSidecarVersion1,
convert: true, // Convert some version 0 blobs to version 1 while retrieving
},
}
for i, c := range cases {
var (
@ -2026,7 +2050,7 @@ func TestGetBlobs(t *testing.T) {
filled[len(vhashes)] = struct{}{}
vhashes = append(vhashes, testrand.Hash())
}
blobs, _, proofs, err := pool.GetBlobs(vhashes, c.version)
blobs, _, proofs, err := pool.GetBlobs(vhashes, c.version, c.convert)
if err != nil {
t.Errorf("Unexpected error for case %d, %v", i, err)
}
@ -2041,6 +2065,7 @@ func TestGetBlobs(t *testing.T) {
var unknown int
for j := 0; j < len(blobs); j++ {
testBlobIndex := c.start + j - unknown
if _, exist := filled[j]; exist {
if blobs[j] != nil || proofs[j] != nil {
t.Errorf("Unexpected blob and proof, item %d", j)
@ -2050,17 +2075,22 @@ func TestGetBlobs(t *testing.T) {
}
// If an item is missing, but shouldn't, error
if blobs[j] == nil || proofs[j] == nil {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
// This is only an error if there was no version mismatch
if c.convert ||
(c.version == types.BlobSidecarVersion1 && 6 <= testBlobIndex && testBlobIndex < 12) ||
(c.version == types.BlobSidecarVersion0 && (testBlobIndex < 6 || 12 <= testBlobIndex)) {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
}
continue
}
// Item retrieved, make sure the blob matches the expectation
if *blobs[j] != *testBlobs[c.start+j-unknown] {
if *blobs[j] != *testBlobs[testBlobIndex] {
t.Errorf("retrieved blob mismatch: item %d, hash %x", j, vhashes[j])
continue
}
// Item retrieved, make sure the proof matches the expectation
if c.version == types.BlobSidecarVersion0 {
if proofs[j][0] != testBlobProofs[c.start+j-unknown] {
if proofs[j][0] != testBlobProofs[testBlobIndex] {
t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
}
} else {
@ -2074,6 +2104,185 @@ func TestGetBlobs(t *testing.T) {
pool.Close()
}
// TestSidecarConversion will verify that after the Osaka fork, all legacy
// sidecars in the pool are successfully convert to v1 sidecars.
func TestSidecarConversion(t *testing.T) {
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true)))
// Create a temporary folder for the persistent backend
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
var (
preOsakaTxs = make(types.Transactions, 10)
postOsakaTxs = make(types.Transactions, 3)
keys = make([]*ecdsa.PrivateKey, len(preOsakaTxs)+len(postOsakaTxs))
addrs = make([]common.Address, len(preOsakaTxs)+len(postOsakaTxs))
statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
)
for i := range keys {
keys[i], _ = crypto.GenerateKey()
addrs[i] = crypto.PubkeyToAddress(keys[i].PublicKey)
statedb.AddBalance(addrs[i], uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
}
for i := range preOsakaTxs {
preOsakaTxs[i] = makeMultiBlobTx(0, 1, 1000, 100, 2, 0, keys[i], types.BlobSidecarVersion0)
}
for i := range postOsakaTxs {
if i == 0 {
// First has a v0 sidecar.
postOsakaTxs[i] = makeMultiBlobTx(0, 1, 1000, 100, 1, 0, keys[len(preOsakaTxs)+i], types.BlobSidecarVersion0)
}
postOsakaTxs[i] = makeMultiBlobTx(0, 1, 1000, 100, 1, 0, keys[len(preOsakaTxs)+i], types.BlobSidecarVersion1)
}
statedb.Commit(0, true, false)
// Test plan:
// 1) Create a bunch v0 sidecar txs and add to pool before Osaka.
// 2) Pass in new Osaka header to activate the conversion thread.
// 3) Continue adding both v0 and v1 transactions to the pool.
// 4) Verify that as additional blocks come in, transactions involved in the
// migration are correctly discarded.
config := &params.ChainConfig{
ChainID: big.NewInt(1),
LondonBlock: big.NewInt(0),
BerlinBlock: big.NewInt(0),
CancunTime: newUint64(0),
PragueTime: newUint64(0),
OsakaTime: newUint64(1),
BlobScheduleConfig: params.DefaultBlobSchedule,
}
chain := &testBlockChain{
config: config,
basefee: uint256.NewInt(1050),
blobfee: uint256.NewInt(105),
statedb: statedb,
blocks: make(map[uint64]*types.Block),
}
// Create 3 blocks:
// - the current block, before Osaka
// - the first block after Osaka
// - another post-Osaka block with several transactions in it
header0 := chain.CurrentBlock()
header0.Time = 0
chain.blocks[0] = types.NewBlockWithHeader(header0)
header1 := chain.CurrentBlock()
header1.Number = big.NewInt(1)
header1.Time = 1
chain.blocks[1] = types.NewBlockWithHeader(header1)
header2 := chain.CurrentBlock()
header2.Time = 2
header2.Number = big.NewInt(2)
// Make a copy of one of the pre-Osaka transactions and convert it to v1 here
// so that we can add it to the pool later and ensure a duplicate is not added
// by the conversion queue.
tx := preOsakaTxs[len(preOsakaTxs)-1]
sc := *tx.BlobTxSidecar() // copy sidecar
sc.ToV1()
tx.WithBlobTxSidecar(&sc)
block2 := types.NewBlockWithHeader(header2).WithBody(types.Body{Transactions: append(postOsakaTxs, tx)})
chain.blocks[2] = block2
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, header0, newReserver()); err != nil {
t.Fatalf("failed to create blob pool: %v", err)
}
errs := pool.Add(preOsakaTxs, true)
for i, err := range errs {
if err != nil {
t.Errorf("failed to insert blob tx from %s: %s", addrs[i], errs[i])
}
}
// Kick off migration.
pool.Reset(header0, header1)
// Add the v0 sidecar tx, but don't block so we can keep doing other stuff
// while it converts the sidecar.
addDone := make(chan struct{})
go func() {
pool.Add(types.Transactions{postOsakaTxs[0]}, false)
close(addDone)
}()
// Add the post-Osaka v1 sidecar txs.
errs = pool.Add(postOsakaTxs[1:], false)
for _, err := range errs {
if err != nil {
t.Fatalf("expected tx add to succeed: %v", err)
}
}
// Wait for the first tx's conversion to complete, then check that all
// transactions added after Osaka can be accounted for in the pool.
<-addDone
pending := pool.Pending(txpool.PendingFilter{BlobTxs: true, BlobVersion: types.BlobSidecarVersion1})
for _, tx := range postOsakaTxs {
from, _ := pool.signer.Sender(tx)
if len(pending[from]) != 1 || pending[from][0].Hash != tx.Hash() {
t.Fatalf("expected post-Osaka txs to be pending")
}
}
// Now update the pool with the next block. This should cause the pool to
// clear out the post-Osaka txs since they were included in block 2. Since the
// test blockchain doesn't manage nonces, we'll just do that manually before
// the reset is called. Don't forget about the pre-Osaka transaction we also
// added to block 2!
for i := range postOsakaTxs {
statedb.SetNonce(addrs[len(preOsakaTxs)+i], 1, tracing.NonceChangeEoACall)
}
statedb.SetNonce(addrs[len(preOsakaTxs)-1], 1, tracing.NonceChangeEoACall)
pool.Reset(header1, block2.Header())
// Now verify no post-Osaka transactions are tracked by the pool.
for i, tx := range postOsakaTxs {
if pool.Get(tx.Hash()) != nil {
t.Fatalf("expected txs added post-osaka to have been placed in limbo due to inclusion in a block: index %d, hash %s", i, tx.Hash())
}
}
// Wait for the pool migration to complete.
<-pool.cQueue.anyBillyConversionDone
// Verify all transactions in the pool were converted and verify the
// subsequent cell proofs.
count, _ := pool.Stats()
if count != len(preOsakaTxs)-1 {
t.Errorf("expected pending count to match initial tx count: pending=%d, expected=%d", count, len(preOsakaTxs)-1)
}
for addr, acc := range pool.index {
for _, m := range acc {
if m.version != types.BlobSidecarVersion1 {
t.Errorf("expected sidecar to have been converted: from %s, hash %s", addr, m.hash)
}
tx := pool.Get(m.hash)
if tx == nil {
t.Errorf("failed to get tx by hash: %s", m.hash)
}
sc := tx.BlobTxSidecar()
if err := kzg4844.VerifyCellProofs(sc.Blobs, sc.Commitments, sc.Proofs); err != nil {
t.Errorf("failed to verify cell proofs for tx %s after conversion: %s", m.hash, err)
}
}
}
verifyPoolInternals(t, pool)
// Launch conversion a second time.
// This is just a sanity check to ensure we can handle it.
pool.Reset(header0, header1)
pool.Close()
}
// fakeBilly is a billy.Database implementation which just drops data on the floor.
type fakeBilly struct {
billy.Database
@ -2157,3 +2366,5 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
}
}
}
func newUint64(val uint64) *uint64 { return &val }

View file

@ -0,0 +1,203 @@
// 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 blobpool
import (
"errors"
"slices"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// maxPendingConversionTasks caps the number of pending conversion tasks. This
// prevents excessive memory usage; the worst-case scenario (2k transactions
// with 6 blobs each) would consume approximately 1.5GB of memory.
const maxPendingConversionTasks = 2048
// txConvert represents a conversion task with an attached legacy blob transaction.
type txConvert struct {
tx *types.Transaction // Legacy blob transaction
done chan error // Channel for signaling back if the conversion succeeds
}
// conversionQueue is a dedicated queue for converting legacy blob transactions
// received from the network after the Osaka fork. Since conversion is expensive,
// it is performed in the background by a single thread, ensuring the main Geth
// process is not overloaded.
type conversionQueue struct {
tasks chan *txConvert
startBilly chan func()
quit chan struct{}
closed chan struct{}
billyQueue []func()
billyTaskDone chan struct{}
// This channel will be closed when the first billy conversion finishes.
// It's added for unit tests to synchronize with the conversion progress.
anyBillyConversionDone chan struct{}
}
// newConversionQueue constructs the conversion queue.
func newConversionQueue() *conversionQueue {
q := &conversionQueue{
tasks: make(chan *txConvert),
startBilly: make(chan func()),
quit: make(chan struct{}),
closed: make(chan struct{}),
anyBillyConversionDone: make(chan struct{}),
}
go q.loop()
return q
}
// convert accepts a legacy blob transaction with version-0 blobs and queues it
// for conversion.
//
// This function may block for a long time until the transaction is processed.
func (q *conversionQueue) convert(tx *types.Transaction) error {
done := make(chan error, 1)
select {
case q.tasks <- &txConvert{tx: tx, done: done}:
return <-done
case <-q.closed:
return errors.New("conversion queue closed")
}
}
// launchBillyConversion starts a conversion task in the background.
func (q *conversionQueue) launchBillyConversion(fn func()) error {
select {
case q.startBilly <- fn:
return nil
case <-q.closed:
return errors.New("conversion queue closed")
}
}
// close terminates the conversion queue.
func (q *conversionQueue) close() {
select {
case <-q.closed:
return
default:
close(q.quit)
<-q.closed
}
}
// run converts a batch of legacy blob txs to the new cell proof format.
func (q *conversionQueue) run(tasks []*txConvert, done chan struct{}, interrupt *atomic.Int32) {
defer close(done)
for _, t := range tasks {
if interrupt != nil && interrupt.Load() != 0 {
t.done <- errors.New("conversion is interrupted")
continue
}
sidecar := t.tx.BlobTxSidecar()
if sidecar == nil {
t.done <- errors.New("tx without sidecar")
continue
}
// Run the conversion, the original sidecar will be mutated in place
start := time.Now()
err := sidecar.ToV1()
t.done <- err
log.Trace("Converted legacy blob tx", "hash", t.tx.Hash(), "err", err, "elapsed", common.PrettyDuration(time.Since(start)))
}
}
func (q *conversionQueue) loop() {
defer close(q.closed)
var (
done chan struct{} // Non-nil if background routine is active
interrupt *atomic.Int32 // Flag to signal conversion interruption
// The pending tasks for sidecar conversion. We assume the number of legacy
// blob transactions requiring conversion will not be excessive. However,
// a hard cap is applied as a protective measure.
txTasks []*txConvert
firstBilly = true
)
for {
select {
case t := <-q.tasks:
if len(txTasks) >= maxPendingConversionTasks {
t.done <- errors.New("conversion queue is overloaded")
continue
}
txTasks = append(txTasks, t)
// Launch the background conversion thread if it's idle
if done == nil {
done, interrupt = make(chan struct{}), new(atomic.Int32)
tasks := slices.Clone(txTasks)
txTasks = txTasks[:0]
go q.run(tasks, done, interrupt)
}
case <-done:
done, interrupt = nil, nil
case fn := <-q.startBilly:
q.billyQueue = append(q.billyQueue, fn)
q.runNextBillyTask()
case <-q.billyTaskDone:
if firstBilly {
close(q.anyBillyConversionDone)
firstBilly = false
}
q.runNextBillyTask()
case <-q.quit:
if done != nil {
log.Debug("Waiting for blob proof conversion to exit")
interrupt.Store(1)
<-done
}
if q.billyTaskDone != nil {
log.Debug("Waiting for blobpool billy conversion to exit")
<-q.billyTaskDone
}
return
}
}
}
func (q *conversionQueue) runNextBillyTask() {
if len(q.billyQueue) == 0 {
q.billyTaskDone = nil
return
}
fn := q.billyQueue[0]
q.billyQueue = append(q.billyQueue[:0], q.billyQueue[1:]...)
done := make(chan struct{})
go func() { defer close(done); fn() }()
q.billyTaskDone = done
}

View file

@ -0,0 +1,101 @@
// 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 blobpool
import (
"crypto/ecdsa"
"crypto/sha256"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// createV1BlobTx creates a blob transaction with version 1 sidecar for testing.
func createV1BlobTx(nonce uint64, key *ecdsa.PrivateKey) *types.Transaction {
blob := &kzg4844.Blob{byte(nonce)}
commitment, _ := kzg4844.BlobToCommitment(blob)
cellProofs, _ := kzg4844.ComputeCellProofs(blob)
blobtx := &types.BlobTx{
ChainID: uint256.MustFromBig(params.MainnetChainConfig.ChainID),
Nonce: nonce,
GasTipCap: uint256.NewInt(1),
GasFeeCap: uint256.NewInt(1000),
Gas: 21000,
BlobFeeCap: uint256.NewInt(100),
BlobHashes: []common.Hash{kzg4844.CalcBlobHashV1(sha256.New(), &commitment)},
Value: uint256.NewInt(100),
Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion1, []kzg4844.Blob{*blob}, []kzg4844.Commitment{commitment}, cellProofs),
}
return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx)
}
func TestConversionQueueBasic(t *testing.T) {
queue := newConversionQueue()
defer queue.close()
key, _ := crypto.GenerateKey()
tx := makeTx(0, 1, 1, 1, key)
if err := queue.convert(tx); err != nil {
t.Fatalf("Expected successful conversion, got error: %v", err)
}
if tx.BlobTxSidecar().Version != types.BlobSidecarVersion1 {
t.Errorf("Expected sidecar version to be %d, got %d", types.BlobSidecarVersion1, tx.BlobTxSidecar().Version)
}
}
func TestConversionQueueV1BlobTx(t *testing.T) {
queue := newConversionQueue()
defer queue.close()
key, _ := crypto.GenerateKey()
tx := createV1BlobTx(0, key)
version := tx.BlobTxSidecar().Version
err := queue.convert(tx)
if err != nil {
t.Fatalf("Expected successful conversion, got error: %v", err)
}
if tx.BlobTxSidecar().Version != version {
t.Errorf("Expected sidecar version to remain %d, got %d", version, tx.BlobTxSidecar().Version)
}
}
func TestConversionQueueClosed(t *testing.T) {
queue := newConversionQueue()
// Close the queue first
queue.close()
key, _ := crypto.GenerateKey()
tx := makeTx(0, 1, 1, 1, key)
err := queue.convert(tx)
if err == nil {
t.Fatal("Expected error when converting on closed queue, got nil")
}
}
func TestConversionQueueDoubleClose(t *testing.T) {
queue := newConversionQueue()
queue.close()
queue.close() // Should not panic
}

View file

@ -110,3 +110,13 @@ func (l *lookup) untrack(tx *blobTxMeta) {
}
}
}
// update updates the transaction index. It should only be used in the conversion.
func (l *lookup) update(hash common.Hash, id uint64, size uint64) bool {
meta, exists := l.txIndex[hash]
if !exists {
return false
}
meta.id, meta.size = id, size
return true
}

View file

@ -23,7 +23,6 @@ import (
"math"
"math/big"
"slices"
"sort"
"sync"
"sync/atomic"
"time"
@ -238,11 +237,10 @@ type LegacyPool struct {
pendingNonces *noncer // Pending state tracking virtual nonces
reserver txpool.Reserver // Address reserver to ensure exclusivity across subpools
pending map[common.Address]*list // All currently processable transactions
queue map[common.Address]*list // Queued but non-processable transactions
beats map[common.Address]time.Time // Last heartbeat from each known account
all *lookup // All transactions to allow lookups
priced *pricedList // All transactions sorted by price
pending map[common.Address]*list // All currently processable transactions
queue *queue
all *lookup // All transactions to allow lookups
priced *pricedList // All transactions sorted by price
reqResetCh chan *txpoolResetRequest
reqPromoteCh chan *accountSet
@ -266,14 +264,14 @@ func New(config Config, chain BlockChain) *LegacyPool {
config = (&config).sanitize()
// Create the transaction pool with its initial settings
signer := types.LatestSigner(chain.Config())
pool := &LegacyPool{
config: config,
chain: chain,
chainconfig: chain.Config(),
signer: types.LatestSigner(chain.Config()),
signer: signer,
pending: make(map[common.Address]*list),
queue: make(map[common.Address]*list),
beats: make(map[common.Address]time.Time),
queue: newQueue(config, signer),
all: newLookup(),
reqResetCh: make(chan *txpoolResetRequest),
reqPromoteCh: make(chan *accountSet),
@ -369,15 +367,8 @@ func (pool *LegacyPool) loop() {
// Handle inactive account transaction eviction
case <-evict.C:
pool.mu.Lock()
for addr := range pool.queue {
// Any old enough should be removed
if time.Since(pool.beats[addr]) > pool.config.Lifetime {
list := pool.queue[addr].Flatten()
for _, tx := range list {
pool.removeTx(tx.Hash(), true, true)
}
queuedEvictionMeter.Mark(int64(len(list)))
}
for _, hash := range pool.queue.evictList() {
pool.removeTx(hash, true, true)
}
pool.mu.Unlock()
}
@ -459,11 +450,7 @@ func (pool *LegacyPool) stats() (int, int) {
for _, list := range pool.pending {
pending += list.Len()
}
queued := 0
for _, list := range pool.queue {
queued += list.Len()
}
return pending, queued
return pending, pool.queue.stats()
}
// Content retrieves the data content of the transaction pool, returning all the
@ -476,10 +463,7 @@ func (pool *LegacyPool) Content() (map[common.Address][]*types.Transaction, map[
for addr, list := range pool.pending {
pending[addr] = list.Flatten()
}
queued := make(map[common.Address][]*types.Transaction, len(pool.queue))
for addr, list := range pool.queue {
queued[addr] = list.Flatten()
}
queued := pool.queue.content()
return pending, queued
}
@ -493,10 +477,7 @@ func (pool *LegacyPool) ContentFrom(addr common.Address) ([]*types.Transaction,
if list, ok := pool.pending[addr]; ok {
pending = list.Flatten()
}
var queued []*types.Transaction
if list, ok := pool.queue[addr]; ok {
queued = list.Flatten()
}
queued := pool.queue.contentFrom(addr)
return pending, queued
}
@ -644,7 +625,7 @@ func (pool *LegacyPool) validateAuth(tx *types.Transaction) error {
if pending := pool.pending[auth]; pending != nil {
count += pending.Len()
}
if queue := pool.queue[auth]; queue != nil {
if queue, ok := pool.queue.get(auth); ok {
count += queue.Len()
}
if count > 1 {
@ -691,7 +672,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) {
// only by this subpool until all transactions are evicted
var (
_, hasPending = pool.pending[from]
_, hasQueued = pool.queue[from]
_, hasQueued = pool.queue.get(from)
)
if !hasPending && !hasQueued {
if err := pool.reserver.Hold(from); err != nil {
@ -790,7 +771,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) {
log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
// Successful promotion, bump the heartbeat
pool.beats[from] = time.Now()
pool.queue.bump(from)
return old != nil, nil
}
// New transaction isn't replacing a pending one, push into queue
@ -815,7 +796,7 @@ func (pool *LegacyPool) isGapped(from common.Address, tx *types.Transaction) boo
}
// The transaction has a nonce gap with pending list, it's only considered
// as executable if transactions in queue can fill up the nonce gap.
queue, ok := pool.queue[from]
queue, ok := pool.queue.get(from)
if !ok {
return true
}
@ -831,25 +812,12 @@ func (pool *LegacyPool) isGapped(from common.Address, tx *types.Transaction) boo
//
// Note, this method assumes the pool lock is held!
func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, addAll bool) (bool, error) {
// Try to insert the transaction into the future queue
from, _ := types.Sender(pool.signer, tx) // already validated
if pool.queue[from] == nil {
pool.queue[from] = newList(false)
replaced, err := pool.queue.add(tx)
if err != nil {
return false, err
}
inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump)
if !inserted {
// An older transaction was better, discard this
queuedDiscardMeter.Mark(1)
return false, txpool.ErrReplaceUnderpriced
}
// Discard any previous transaction and mark this
if old != nil {
pool.all.Remove(old.Hash())
pool.priced.Removed(1)
queuedReplaceMeter.Mark(1)
} else {
// Nothing was replaced, bump the queued counter
queuedGauge.Inc(1)
if replaced != nil {
pool.removeTx(*replaced, true, true)
}
// If the transaction isn't in lookup set but it's expected to be there,
// show the error log.
@ -860,11 +828,7 @@ func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, addAl
pool.all.Add(tx)
pool.priced.Put(tx)
}
// If we never record the heartbeat, do it right now.
if _, exist := pool.beats[from]; !exist {
pool.beats[from] = time.Now()
}
return old != nil, nil
return replaced != nil, nil
}
// promoteTx adds a transaction to the pending (processable) list of transactions
@ -899,7 +863,7 @@ func (pool *LegacyPool) promoteTx(addr common.Address, hash common.Hash, tx *typ
pool.pendingNonces.set(addr, tx.Nonce()+1)
// Successful promotion, bump the heartbeat
pool.beats[addr] = time.Now()
pool.queue.bump(addr)
return true
}
@ -984,17 +948,24 @@ func (pool *LegacyPool) Add(txs []*types.Transaction, sync bool) []error {
// addTxsLocked attempts to queue a batch of transactions if they are valid.
// 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) {
dirty := newAccountSet(pool.signer)
errs := make([]error, len(txs))
var (
dirty = newAccountSet(pool.signer)
errs = make([]error, len(txs))
valid int64
)
for i, tx := range txs {
replaced, err := pool.add(tx)
errs[i] = err
if err == nil && !replaced {
dirty.addTx(tx)
if err == nil {
if !replaced {
dirty.addTx(tx)
}
valid++
}
}
validTxMeter.Mark(int64(len(dirty.accounts)))
validTxMeter.Mark(valid)
return errs, dirty
}
@ -1012,7 +983,7 @@ func (pool *LegacyPool) Status(hash common.Hash) txpool.TxStatus {
if txList := pool.pending[from]; txList != nil && txList.txs.items[tx.Nonce()] != nil {
return txpool.TxStatusPending
} else if txList := pool.queue[from]; txList != nil && txList.txs.items[tx.Nonce()] != nil {
} else if txList, ok := pool.queue.get(from); ok && txList.txs.items[tx.Nonce()] != nil {
return txpool.TxStatusQueued
}
return txpool.TxStatusUnknown
@ -1089,7 +1060,7 @@ func (pool *LegacyPool) removeTx(hash common.Hash, outofbound bool, unreserve bo
defer func() {
var (
_, hasPending = pool.pending[addr]
_, hasQueued = pool.queue[addr]
_, hasQueued = pool.queue.get(addr)
)
if !hasPending && !hasQueued {
pool.reserver.Release(addr)
@ -1121,16 +1092,7 @@ func (pool *LegacyPool) removeTx(hash common.Hash, outofbound bool, unreserve bo
}
}
// Transaction is in the future queue
if future := pool.queue[addr]; future != nil {
if removed, _ := future.Remove(tx); removed {
// Reduce the queued counter
queuedGauge.Dec(1)
}
if future.Empty() {
delete(pool.queue, addr)
delete(pool.beats, addr)
}
}
pool.queue.remove(addr, tx)
return 0
}
@ -1278,10 +1240,7 @@ func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest,
}
}
// Reset needs promote for all addresses
promoteAddrs = make([]common.Address, 0, len(pool.queue))
for addr := range pool.queue {
promoteAddrs = append(promoteAddrs, addr)
}
promoteAddrs = pool.queue.addresses()
}
// Check for pending transactions for every account that sent new ones
promoted := pool.promoteExecutables(promoteAddrs)
@ -1435,60 +1394,29 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) {
// future queue to the set of pending transactions. During this process, all
// invalidated transactions (low nonce, low balance) are deleted.
func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.Transaction {
// Track the promoted transactions to broadcast them at once
var promoted []*types.Transaction
// Iterate over all accounts and promote any executable transactions
gasLimit := pool.currentHead.Load().GasLimit
for _, addr := range accounts {
list := pool.queue[addr]
if list == nil {
continue // Just in case someone calls with a non existing account
}
// Drop all transactions that are deemed too old (low nonce)
forwards := list.Forward(pool.currentState.GetNonce(addr))
for _, tx := range forwards {
pool.all.Remove(tx.Hash())
}
log.Trace("Removed old queued transactions", "count", len(forwards))
// Drop all transactions that are too costly (low balance or out of gas)
drops, _ := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
for _, tx := range drops {
pool.all.Remove(tx.Hash())
}
log.Trace("Removed unpayable queued transactions", "count", len(drops))
queuedNofundsMeter.Mark(int64(len(drops)))
promotable, dropped, removedAddresses := pool.queue.promoteExecutables(accounts, gasLimit, pool.currentState, pool.pendingNonces)
// Gather all executable transactions and promote them
readies := list.Ready(pool.pendingNonces.get(addr))
for _, tx := range readies {
hash := tx.Hash()
if pool.promoteTx(addr, hash, tx) {
promoted = append(promoted, tx)
}
// promote all promotable transactions
promoted := make([]*types.Transaction, 0, len(promotable))
for _, tx := range promotable {
from, _ := pool.signer.Sender(tx)
if pool.promoteTx(from, tx.Hash(), tx) {
promoted = append(promoted, tx)
}
log.Trace("Promoted queued transactions", "count", len(promoted))
queuedGauge.Dec(int64(len(readies)))
}
// Drop all transactions over the allowed limit
var caps = list.Cap(int(pool.config.AccountQueue))
for _, tx := range caps {
hash := tx.Hash()
pool.all.Remove(hash)
log.Trace("Removed cap-exceeding queued transaction", "hash", hash)
}
queuedRateLimitMeter.Mark(int64(len(caps)))
// Mark all the items dropped as removed
pool.priced.Removed(len(forwards) + len(drops) + len(caps))
queuedGauge.Dec(int64(len(forwards) + len(drops) + len(caps)))
// remove all removable transactions
for _, hash := range dropped {
pool.all.Remove(hash)
}
pool.priced.Removed(len(dropped))
// Delete the entire queue entry if it became empty.
if list.Empty() {
delete(pool.queue, addr)
delete(pool.beats, addr)
if _, ok := pool.pending[addr]; !ok {
pool.reserver.Release(addr)
}
// release all accounts that have no more transactions in the pool
for _, addr := range removedAddresses {
_, hasPending := pool.pending[addr]
if !hasPending {
pool.reserver.Release(addr)
}
}
return promoted
@ -1578,43 +1506,18 @@ func (pool *LegacyPool) truncatePending() {
// truncateQueue drops the oldest transactions in the queue if the pool is above the global queue limit.
func (pool *LegacyPool) truncateQueue() {
queued := uint64(0)
for _, list := range pool.queue {
queued += uint64(list.Len())
}
if queued <= pool.config.GlobalQueue {
return
removed, removedAddresses := pool.queue.truncate()
// Remove all removable transactions from the lookup and global price list
for _, hash := range removed {
pool.all.Remove(hash)
}
pool.priced.Removed(len(removed))
// Sort all accounts with queued transactions by heartbeat
addresses := make(addressesByHeartbeat, 0, len(pool.queue))
for addr := range pool.queue {
addresses = append(addresses, addressByHeartbeat{addr, pool.beats[addr]})
}
sort.Sort(sort.Reverse(addresses))
// Drop transactions until the total is below the limit
for drop := queued - pool.config.GlobalQueue; drop > 0 && len(addresses) > 0; {
addr := addresses[len(addresses)-1]
list := pool.queue[addr.address]
addresses = addresses[:len(addresses)-1]
// Drop all transactions if they are less than the overflow
if size := uint64(list.Len()); size <= drop {
for _, tx := range list.Flatten() {
pool.removeTx(tx.Hash(), true, true)
}
drop -= size
queuedRateLimitMeter.Mark(int64(size))
continue
}
// Otherwise drop only last few transactions
txs := list.Flatten()
for i := len(txs) - 1; i >= 0 && drop > 0; i-- {
pool.removeTx(txs[i].Hash(), true, true)
drop--
queuedRateLimitMeter.Mark(1)
for _, addr := range removedAddresses {
_, hasPending := pool.pending[addr]
if !hasPending {
pool.reserver.Release(addr)
}
}
}
@ -1672,25 +1575,13 @@ func (pool *LegacyPool) demoteUnexecutables() {
// Delete the entire pending entry if it became empty.
if list.Empty() {
delete(pool.pending, addr)
if _, ok := pool.queue[addr]; !ok {
if _, ok := pool.queue.get(addr); !ok {
pool.reserver.Release(addr)
}
}
}
}
// addressByHeartbeat is an account address tagged with its last activity timestamp.
type addressByHeartbeat struct {
address common.Address
heartbeat time.Time
}
type addressesByHeartbeat []addressByHeartbeat
func (a addressesByHeartbeat) Len() int { return len(a) }
func (a addressesByHeartbeat) Less(i, j int) bool { return a[i].heartbeat.Before(a[j].heartbeat) }
func (a addressesByHeartbeat) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// accountSet is simply a set of addresses to check for existence, and a signer
// capable of deriving addresses from transactions.
type accountSet struct {
@ -1931,17 +1822,17 @@ func (pool *LegacyPool) Clear() {
// acquire the subpool lock until the transaction addition is completed.
for addr := range pool.pending {
if _, ok := pool.queue[addr]; !ok {
if _, ok := pool.queue.get(addr); !ok {
pool.reserver.Release(addr)
}
}
for addr := range pool.queue {
for _, addr := range pool.queue.addresses() {
pool.reserver.Release(addr)
}
pool.all.Clear()
pool.priced.Reheap()
pool.pending = make(map[common.Address]*list)
pool.queue = make(map[common.Address]*list)
pool.queue = newQueue(pool.config, pool.signer)
pool.pendingNonces = newNoncer(pool.currentState)
}

View file

@ -466,8 +466,8 @@ func TestQueue(t *testing.T) {
if _, ok := pool.pending[from].txs.items[tx.Nonce()]; ok {
t.Error("expected transaction to be in tx pool")
}
if len(pool.queue) > 0 {
t.Error("expected transaction queue to be empty. is", len(pool.queue))
if len(pool.queue.queued) > 0 {
t.Error("expected transaction queue to be empty. is", len(pool.queue.queued))
}
}
@ -492,8 +492,8 @@ func TestQueue2(t *testing.T) {
if len(pool.pending) != 1 {
t.Error("expected pending length to be 1, got", len(pool.pending))
}
if pool.queue[from].Len() != 2 {
t.Error("expected len(queue) == 2, got", pool.queue[from].Len())
if list, _ := pool.queue.get(from); list.Len() != 2 {
t.Error("expected len(queue) == 2, got", list.Len())
}
}
@ -639,8 +639,8 @@ func TestMissingNonce(t *testing.T) {
if len(pool.pending) != 0 {
t.Error("expected 0 pending transactions, got", len(pool.pending))
}
if pool.queue[addr].Len() != 1 {
t.Error("expected 1 queued transaction, got", pool.queue[addr].Len())
if list, _ := pool.queue.get(addr); list.Len() != 1 {
t.Error("expected 1 queued transaction, got", list.Len())
}
if pool.all.Count() != 1 {
t.Error("expected 1 total transactions, got", pool.all.Count())
@ -712,8 +712,8 @@ func TestDropping(t *testing.T) {
if pool.pending[account].Len() != 3 {
t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 3)
}
if pool.queue[account].Len() != 3 {
t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 3)
if list, _ := pool.queue.get(account); list.Len() != 3 {
t.Errorf("queued transaction mismatch: have %d, want %d", list.Len(), 3)
}
if pool.all.Count() != 6 {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 6)
@ -722,8 +722,8 @@ func TestDropping(t *testing.T) {
if pool.pending[account].Len() != 3 {
t.Errorf("pending transaction mismatch: have %d, want %d", pool.pending[account].Len(), 3)
}
if pool.queue[account].Len() != 3 {
t.Errorf("queued transaction mismatch: have %d, want %d", pool.queue[account].Len(), 3)
if list, _ := pool.queue.get(account); list.Len() != 3 {
t.Errorf("queued transaction mismatch: have %d, want %d", list.Len(), 3)
}
if pool.all.Count() != 6 {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 6)
@ -741,13 +741,14 @@ func TestDropping(t *testing.T) {
if _, ok := pool.pending[account].txs.items[tx2.Nonce()]; ok {
t.Errorf("out-of-fund pending transaction present: %v", tx1)
}
if _, ok := pool.queue[account].txs.items[tx10.Nonce()]; !ok {
list, _ := pool.queue.get(account)
if _, ok := list.txs.items[tx10.Nonce()]; !ok {
t.Errorf("funded queued transaction missing: %v", tx10)
}
if _, ok := pool.queue[account].txs.items[tx11.Nonce()]; !ok {
if _, ok := list.txs.items[tx11.Nonce()]; !ok {
t.Errorf("funded queued transaction missing: %v", tx10)
}
if _, ok := pool.queue[account].txs.items[tx12.Nonce()]; ok {
if _, ok := list.txs.items[tx12.Nonce()]; ok {
t.Errorf("out-of-fund queued transaction present: %v", tx11)
}
if pool.all.Count() != 4 {
@ -763,10 +764,11 @@ func TestDropping(t *testing.T) {
if _, ok := pool.pending[account].txs.items[tx1.Nonce()]; ok {
t.Errorf("over-gased pending transaction present: %v", tx1)
}
if _, ok := pool.queue[account].txs.items[tx10.Nonce()]; !ok {
list, _ = pool.queue.get(account)
if _, ok := list.txs.items[tx10.Nonce()]; !ok {
t.Errorf("funded queued transaction missing: %v", tx10)
}
if _, ok := pool.queue[account].txs.items[tx11.Nonce()]; ok {
if _, ok := list.txs.items[tx11.Nonce()]; ok {
t.Errorf("over-gased queued transaction present: %v", tx11)
}
if pool.all.Count() != 2 {
@ -820,8 +822,8 @@ func TestPostponing(t *testing.T) {
if pending := pool.pending[accs[0]].Len() + pool.pending[accs[1]].Len(); pending != len(txs) {
t.Errorf("pending transaction mismatch: have %d, want %d", pending, len(txs))
}
if len(pool.queue) != 0 {
t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue), 0)
if len(pool.queue.addresses()) != 0 {
t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue.addresses()), 0)
}
if pool.all.Count() != len(txs) {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), len(txs))
@ -830,8 +832,8 @@ func TestPostponing(t *testing.T) {
if pending := pool.pending[accs[0]].Len() + pool.pending[accs[1]].Len(); pending != len(txs) {
t.Errorf("pending transaction mismatch: have %d, want %d", pending, len(txs))
}
if len(pool.queue) != 0 {
t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue), 0)
if len(pool.queue.addresses()) != 0 {
t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue.addresses()), 0)
}
if pool.all.Count() != len(txs) {
t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), len(txs))
@ -847,7 +849,8 @@ func TestPostponing(t *testing.T) {
if _, ok := pool.pending[accs[0]].txs.items[txs[0].Nonce()]; !ok {
t.Errorf("tx %d: valid and funded transaction missing from pending pool: %v", 0, txs[0])
}
if _, ok := pool.queue[accs[0]].txs.items[txs[0].Nonce()]; ok {
list, _ := pool.queue.get(accs[0])
if _, ok := list.txs.items[txs[0].Nonce()]; ok {
t.Errorf("tx %d: valid and funded transaction present in future queue: %v", 0, txs[0])
}
for i, tx := range txs[1:100] {
@ -855,14 +858,14 @@ func TestPostponing(t *testing.T) {
if _, ok := pool.pending[accs[0]].txs.items[tx.Nonce()]; ok {
t.Errorf("tx %d: valid but future transaction present in pending pool: %v", i+1, tx)
}
if _, ok := pool.queue[accs[0]].txs.items[tx.Nonce()]; !ok {
if _, ok := list.txs.items[tx.Nonce()]; !ok {
t.Errorf("tx %d: valid but future transaction missing from future queue: %v", i+1, tx)
}
} else {
if _, ok := pool.pending[accs[0]].txs.items[tx.Nonce()]; ok {
t.Errorf("tx %d: out-of-fund transaction present in pending pool: %v", i+1, tx)
}
if _, ok := pool.queue[accs[0]].txs.items[tx.Nonce()]; ok {
if _, ok := list.txs.items[tx.Nonce()]; ok {
t.Errorf("tx %d: out-of-fund transaction present in future queue: %v", i+1, tx)
}
}
@ -872,13 +875,14 @@ func TestPostponing(t *testing.T) {
if pool.pending[accs[1]] != nil {
t.Errorf("invalidated account still has pending transactions")
}
list, _ = pool.queue.get(accs[1])
for i, tx := range txs[100:] {
if i%2 == 1 {
if _, ok := pool.queue[accs[1]].txs.items[tx.Nonce()]; !ok {
if _, ok := list.txs.items[tx.Nonce()]; !ok {
t.Errorf("tx %d: valid but future transaction missing from future queue: %v", 100+i, tx)
}
} else {
if _, ok := pool.queue[accs[1]].txs.items[tx.Nonce()]; ok {
if _, ok := list.txs.items[tx.Nonce()]; ok {
t.Errorf("tx %d: out-of-fund transaction present in future queue: %v", 100+i, tx)
}
}
@ -963,13 +967,14 @@ func TestQueueAccountLimiting(t *testing.T) {
if len(pool.pending) != 0 {
t.Errorf("tx %d: pending pool size mismatch: have %d, want %d", i, len(pool.pending), 0)
}
list, _ := pool.queue.get(account)
if i <= testTxPoolConfig.AccountQueue {
if pool.queue[account].Len() != int(i) {
t.Errorf("tx %d: queue size mismatch: have %d, want %d", i, pool.queue[account].Len(), i)
if list.Len() != int(i) {
t.Errorf("tx %d: queue size mismatch: have %d, want %d", i, list.Len(), i)
}
} else {
if pool.queue[account].Len() != int(testTxPoolConfig.AccountQueue) {
t.Errorf("tx %d: queue limit mismatch: have %d, want %d", i, pool.queue[account].Len(), testTxPoolConfig.AccountQueue)
if list.Len() != int(testTxPoolConfig.AccountQueue) {
t.Errorf("tx %d: queue limit mismatch: have %d, want %d", i, list.Len(), testTxPoolConfig.AccountQueue)
}
}
}
@ -1020,7 +1025,7 @@ func TestQueueGlobalLimiting(t *testing.T) {
pool.addRemotesSync(txs)
queued := 0
for addr, list := range pool.queue {
for addr, list := range pool.queue.queued {
if list.Len() > int(config.AccountQueue) {
t.Errorf("addr %x: queued accounts overflown allowance: %d > %d", addr, list.Len(), config.AccountQueue)
}
@ -1179,8 +1184,8 @@ func TestPendingLimiting(t *testing.T) {
if pool.pending[account].Len() != int(i)+1 {
t.Errorf("tx %d: pending pool size mismatch: have %d, want %d", i, pool.pending[account].Len(), i+1)
}
if len(pool.queue) != 0 {
t.Errorf("tx %d: queue size mismatch: have %d, want %d", i, pool.queue[account].Len(), 0)
if len(pool.queue.addresses()) != 0 {
t.Errorf("tx %d: queue size mismatch: have %d, want %d", i, len(pool.queue.addresses()), 0)
}
}
if pool.all.Count() != int(testTxPoolConfig.AccountQueue+5) {

View file

@ -0,0 +1,275 @@
// 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 legacypool
import (
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// queue manages nonce-gapped transactions that have been validated but are
// not yet processable.
type queue struct {
config Config
signer types.Signer
queued map[common.Address]*list // Queued but non-processable transactions
beats map[common.Address]time.Time // Last heartbeat from each known account
}
func newQueue(config Config, signer types.Signer) *queue {
return &queue{
signer: signer,
config: config,
queued: make(map[common.Address]*list),
beats: make(map[common.Address]time.Time),
}
}
// evictList returns the hashes of transactions that are old enough to be evicted.
func (q *queue) evictList() []common.Hash {
var removed []common.Hash
for addr, list := range q.queued {
if time.Since(q.beats[addr]) > q.config.Lifetime {
for _, tx := range list.Flatten() {
removed = append(removed, tx.Hash())
}
}
}
queuedEvictionMeter.Mark(int64(len(removed)))
return removed
}
func (q *queue) stats() int {
queued := 0
for _, list := range q.queued {
queued += list.Len()
}
return queued
}
func (q *queue) content() map[common.Address][]*types.Transaction {
queued := make(map[common.Address][]*types.Transaction, len(q.queued))
for addr, list := range q.queued {
queued[addr] = list.Flatten()
}
return queued
}
func (q *queue) contentFrom(addr common.Address) []*types.Transaction {
var queued []*types.Transaction
if list, ok := q.get(addr); ok {
queued = list.Flatten()
}
return queued
}
func (q *queue) get(addr common.Address) (*list, bool) {
l, ok := q.queued[addr]
return l, ok
}
func (q *queue) bump(addr common.Address) {
q.beats[addr] = time.Now()
}
func (q *queue) addresses() []common.Address {
addrs := make([]common.Address, 0, len(q.queued))
for addr := range q.queued {
addrs = append(addrs, addr)
}
return addrs
}
func (q *queue) remove(addr common.Address, tx *types.Transaction) {
if future := q.queued[addr]; future != nil {
if txOld := future.txs.Get(tx.Nonce()); txOld != nil && txOld.Hash() != tx.Hash() {
// Edge case, a different transaction
// with the same nonce is in the queued, just ignore
return
}
if removed, _ := future.Remove(tx); removed {
// Reduce the queued counter
queuedGauge.Dec(1)
}
if future.Empty() {
delete(q.queued, addr)
delete(q.beats, addr)
}
}
}
func (q *queue) add(tx *types.Transaction) (*common.Hash, error) {
// Try to insert the transaction into the future queue
from, _ := types.Sender(q.signer, tx) // already validated
if q.queued[from] == nil {
q.queued[from] = newList(false)
}
inserted, old := q.queued[from].Add(tx, q.config.PriceBump)
if !inserted {
// An older transaction was better, discard this
queuedDiscardMeter.Mark(1)
return nil, txpool.ErrReplaceUnderpriced
}
// If we never record the heartbeat, do it right now.
if _, exist := q.beats[from]; !exist {
q.beats[from] = time.Now()
}
if old == nil {
// Nothing was replaced, bump the queued counter
queuedGauge.Inc(1)
return nil, nil
}
h := old.Hash()
// Transaction was replaced, bump the replacement counter
queuedReplaceMeter.Mark(1)
return &h, nil
}
// promoteExecutables iterates over all accounts with queued transactions, selecting
// for promotion any that are now executable. It also drops any transactions that are
// deemed too old (nonce too low) or too costly (insufficient funds or over gas limit).
//
// Returns three lists:
// - all transactions that were removed from the queue and selected for promotion;
// - all other transactions that were removed from the queue and dropped;
// - the list of addresses removed.
func (q *queue) promoteExecutables(accounts []common.Address, gasLimit uint64, currentState *state.StateDB, nonces *noncer) ([]*types.Transaction, []common.Hash, []common.Address) {
// Track the promotable transactions to broadcast them at once
var (
promotable []*types.Transaction
dropped []common.Hash
removedAddresses []common.Address
)
// Iterate over all accounts and promote any executable transactions
for _, addr := range accounts {
list := q.queued[addr]
if list == nil {
continue // Just in case someone calls with a non existing account
}
// Drop all transactions that are deemed too old (low nonce)
forwards := list.Forward(currentState.GetNonce(addr))
for _, tx := range forwards {
dropped = append(dropped, tx.Hash())
}
log.Trace("Removing old queued transactions", "count", len(forwards))
// Drop all transactions that are too costly (low balance or out of gas)
drops, _ := list.Filter(currentState.GetBalance(addr), gasLimit)
for _, tx := range drops {
dropped = append(dropped, tx.Hash())
}
log.Trace("Removing unpayable queued transactions", "count", len(drops))
queuedNofundsMeter.Mark(int64(len(drops)))
// Gather all executable transactions and promote them
readies := list.Ready(nonces.get(addr))
promotable = append(promotable, readies...)
log.Trace("Promoting queued transactions", "count", len(promotable))
queuedGauge.Dec(int64(len(readies)))
// Drop all transactions over the allowed limit
var caps = list.Cap(int(q.config.AccountQueue))
for _, tx := range caps {
hash := tx.Hash()
dropped = append(dropped, hash)
log.Trace("Removing cap-exceeding queued transaction", "hash", hash)
}
queuedRateLimitMeter.Mark(int64(len(caps)))
// Delete the entire queue entry if it became empty.
if list.Empty() {
delete(q.queued, addr)
delete(q.beats, addr)
removedAddresses = append(removedAddresses, addr)
}
}
queuedGauge.Dec(int64(len(dropped)))
return promotable, dropped, removedAddresses
}
// truncate drops the oldest transactions from the queue until the total
// number is below the configured limit. Returns the hashes of all dropped
// transactions and the addresses of accounts that became empty due to
// the truncation.
func (q *queue) truncate() ([]common.Hash, []common.Address) {
queued := uint64(0)
for _, list := range q.queued {
queued += uint64(list.Len())
}
if queued <= q.config.GlobalQueue {
return nil, nil
}
// Sort all accounts with queued transactions by heartbeat
addresses := make(addressesByHeartbeat, 0, len(q.queued))
for addr := range q.queued {
addresses = append(addresses, addressByHeartbeat{addr, q.beats[addr]})
}
sort.Sort(sort.Reverse(addresses))
// Drop transactions until the total is below the limit
var (
removed = make([]common.Hash, 0)
removedAddresses = make([]common.Address, 0)
)
for drop := queued - q.config.GlobalQueue; drop > 0 && len(addresses) > 0; {
addr := addresses[len(addresses)-1]
list := q.queued[addr.address]
addresses = addresses[:len(addresses)-1]
// Drop all transactions if they are less than the overflow
if size := uint64(list.Len()); size <= drop {
for _, tx := range list.Flatten() {
q.remove(addr.address, tx)
removed = append(removed, tx.Hash())
}
drop -= size
queuedRateLimitMeter.Mark(int64(size))
removedAddresses = append(removedAddresses, addr.address)
continue
}
// Otherwise drop only last few transactions
txs := list.Flatten()
for i := len(txs) - 1; i >= 0 && drop > 0; i-- {
q.remove(addr.address, txs[i])
removed = append(removed, txs[i].Hash())
drop--
queuedRateLimitMeter.Mark(1)
}
}
// No need to clear empty accounts, remove already does that
return removed, removedAddresses
}
// addressByHeartbeat is an account address tagged with its last activity timestamp.
type addressByHeartbeat struct {
address common.Address
heartbeat time.Time
}
type addressesByHeartbeat []addressByHeartbeat
func (a addressesByHeartbeat) Len() int { return len(a) }
func (a addressesByHeartbeat) Less(i, j int) bool { return a[i].heartbeat.Before(a[j].heartbeat) }
func (a addressesByHeartbeat) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

View file

@ -117,6 +117,25 @@ func (journal *journal) load(add func([]*types.Transaction) []error) error {
return failure
}
func (journal *journal) setupWriter() error {
if journal.writer != nil {
if err := journal.writer.Close(); err != nil {
return err
}
journal.writer = nil
}
// Re-open the journal file for appending
// Use O_APPEND to ensure we always write to the end of the file
sink, err := os.OpenFile(journal.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
journal.writer = sink
return nil
}
// insert adds the specified transaction to the local disk journal.
func (journal *journal) insert(tx *types.Transaction) error {
if journal.writer == nil {
@ -177,7 +196,6 @@ func (journal *journal) rotate(all map[common.Address]types.Transactions) error
// close flushes the transaction journal contents to disk and closes the file.
func (journal *journal) close() error {
var err error
if journal.writer != nil {
err = journal.writer.Close()
journal.writer = nil

View file

@ -114,13 +114,14 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
}
// recheck checks and returns any transactions that needs to be resubmitted.
func (tracker *TxTracker) recheck(journalCheck bool) (resubmits []*types.Transaction, rejournal map[common.Address]types.Transactions) {
func (tracker *TxTracker) recheck(journalCheck bool) []*types.Transaction {
tracker.mu.Lock()
defer tracker.mu.Unlock()
var (
numStales = 0
numOk = 0
resubmits []*types.Transaction
)
for sender, txs := range tracker.byAddr {
// Wipe the stales
@ -141,7 +142,7 @@ func (tracker *TxTracker) recheck(journalCheck bool) (resubmits []*types.Transac
}
if journalCheck { // rejournal
rejournal = make(map[common.Address]types.Transactions)
rejournal := make(map[common.Address]types.Transactions)
for _, tx := range tracker.all {
addr, _ := types.Sender(tracker.signer, tx)
rejournal[addr] = append(rejournal[addr], tx)
@ -153,10 +154,18 @@ func (tracker *TxTracker) recheck(journalCheck bool) (resubmits []*types.Transac
return int(a.Nonce() - b.Nonce())
})
}
// Rejournal the tracker while holding the lock. No new transactions will
// be added to the old journal during this period, preventing any potential
// transaction loss.
if tracker.journal != nil {
if err := tracker.journal.rotate(rejournal); err != nil {
log.Warn("Transaction journal rotation failed", "err", err)
}
}
}
localGauge.Update(int64(len(tracker.all)))
log.Debug("Tx tracker status", "need-resubmit", len(resubmits), "stale", numStales, "ok", numOk)
return resubmits, rejournal
return resubmits
}
// Start implements node.Lifecycle interface
@ -185,6 +194,12 @@ func (tracker *TxTracker) loop() {
tracker.TrackAll(transactions)
return nil
})
// Setup the writer for the upcoming transactions
if err := tracker.journal.setupWriter(); err != nil {
log.Error("Failed to setup the journal writer", "err", err)
return
}
defer tracker.journal.close()
}
var (
@ -196,20 +211,15 @@ func (tracker *TxTracker) loop() {
case <-tracker.shutdownCh:
return
case <-timer.C:
checkJournal := tracker.journal != nil && time.Since(lastJournal) > tracker.rejournal
resubmits, rejournal := tracker.recheck(checkJournal)
var rejournal bool
if tracker.journal != nil && time.Since(lastJournal) > tracker.rejournal {
rejournal, lastJournal = true, time.Now()
log.Debug("Rejournal the transaction tracker")
}
resubmits := tracker.recheck(rejournal)
if len(resubmits) > 0 {
tracker.pool.Add(resubmits, false)
}
if checkJournal {
// Lock to prevent journal.rotate <-> journal.insert (via TrackAll) conflicts
tracker.mu.Lock()
lastJournal = time.Now()
if err := tracker.journal.rotate(rejournal); err != nil {
log.Warn("Transaction journal rotation failed", "err", err)
}
tracker.mu.Unlock()
}
timer.Reset(recheckInterval)
}
}

View file

@ -17,7 +17,11 @@
package locals
import (
"fmt"
"maps"
"math/big"
"math/rand"
"path/filepath"
"testing"
"time"
@ -146,20 +150,59 @@ func TestResubmit(t *testing.T) {
txsA := txs[:len(txs)/2]
txsB := txs[len(txs)/2:]
env.pool.Add(txsA, true)
pending, queued := env.pool.ContentFrom(address)
if len(pending) != len(txsA) || len(queued) != 0 {
t.Fatalf("Unexpected txpool content: %d, %d", len(pending), len(queued))
}
env.tracker.TrackAll(txs)
resubmit, all := env.tracker.recheck(true)
resubmit := env.tracker.recheck(true)
if len(resubmit) != len(txsB) {
t.Fatalf("Unexpected transactions to resubmit, got: %d, want: %d", len(resubmit), len(txsB))
}
if len(all) == 0 || len(all[address]) == 0 {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", 0, len(txs))
}
if len(all[address]) != len(txs) {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", len(all[address]), len(txs))
env.tracker.mu.Lock()
allCopy := maps.Clone(env.tracker.all)
env.tracker.mu.Unlock()
if len(allCopy) != len(txs) {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", len(allCopy), len(txs))
}
}
func TestJournal(t *testing.T) {
journalPath := filepath.Join(t.TempDir(), fmt.Sprintf("%d", rand.Int63()))
env := newTestEnv(t, 10, 0, journalPath)
defer env.close()
env.tracker.Start()
defer env.tracker.Stop()
txs := env.makeTxs(10)
txsA := txs[:len(txs)/2]
txsB := txs[len(txs)/2:]
env.pool.Add(txsA, true)
pending, queued := env.pool.ContentFrom(address)
if len(pending) != len(txsA) || len(queued) != 0 {
t.Fatalf("Unexpected txpool content: %d, %d", len(pending), len(queued))
}
env.tracker.TrackAll(txsA)
env.tracker.TrackAll(txsB)
env.tracker.recheck(true) // manually rejournal the tracker
// Make sure all the transactions are properly journalled
trackerB := New(journalPath, time.Minute, gspec.Config, env.pool)
trackerB.journal.load(func(transactions []*types.Transaction) []error {
trackerB.TrackAll(transactions)
return nil
})
trackerB.mu.Lock()
allCopy := maps.Clone(trackerB.all)
trackerB.mu.Unlock()
if len(allCopy) != len(txs) {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", len(allCopy), len(txs))
}
}

View file

@ -65,7 +65,6 @@ type BlockChain interface {
type TxPool struct {
subpools []SubPool // List of subpools for specialized transaction handling
chain BlockChain
signer types.Signer
stateLock sync.RWMutex // The lock for protecting state instance
state *state.StateDB // Current state at the blockchain head
@ -98,7 +97,6 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) {
pool := &TxPool{
subpools: subpools,
chain: chain,
signer: types.LatestSigner(chain.Config()),
state: statedb,
quit: make(chan chan error),
term: make(chan struct{}),

View file

@ -113,6 +113,10 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if _, err := types.Sender(signer, tx); err != nil {
return fmt.Errorf("%w: %v", ErrInvalidSender, err)
}
// Limit nonce to 2^64-1 per EIP-2681
if tx.Nonce()+1 < tx.Nonce() {
return core.ErrNonceMax
}
// Ensure the transaction has more gas than the bare minimum needed to cover
// the transaction metadata
intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai)
@ -144,7 +148,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
return nil
}
func ValidateBlobSidecar(tx *types.Transaction, sidecar *types.BlobTxCellSidecar, head *types.Header, opts *ValidationOptions) error {
func ValidateBlobSidecar(tx *types.Transaction, sidecar *types.BlobTxCellSidecar, opts *ValidationOptions) error {
if sidecar.Custody.OneCount() == 0 {
return errors.New("blobless blob transaction")
}
@ -171,16 +175,14 @@ func ValidateBlobSidecar(tx *types.Transaction, sidecar *types.BlobTxCellSidecar
return err
}
// Fork-specific sidecar checks, including proof verification.
if opts.Config.IsOsaka(head.Number, head.Time) {
if sidecar.Version == types.BlobSidecarVersion1 {
return validateBlobSidecarOsaka(sidecar, hashes)
} else {
return validateBlobSidecarLegacy(sidecar, hashes)
}
return validateBlobSidecarLegacy(sidecar, hashes)
}
func validateBlobSidecarLegacy(sidecar *types.BlobTxCellSidecar, hashes []common.Hash) error {
if sidecar.Version != types.BlobSidecarVersion0 {
return fmt.Errorf("invalid sidecar version pre-osaka: %v", sidecar.Version)
}
if len(sidecar.Proofs) != len(hashes) {
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes))
}
@ -197,9 +199,6 @@ func validateBlobSidecarLegacy(sidecar *types.BlobTxCellSidecar, hashes []common
}
func validateBlobSidecarOsaka(sidecar *types.BlobTxCellSidecar, hashes []common.Hash) error {
if sidecar.Version != types.BlobSidecarVersion1 {
return fmt.Errorf("invalid sidecar version post-osaka: %v", sidecar.Version)
}
if len(sidecar.Proofs) != len(hashes)*kzg4844.CellProofsPerBlob {
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)*kzg4844.CellProofsPerBlob)
}

Some files were not shown because too many files have changed in this diff Show more