mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 05:36:46 +00:00
Merge remote-tracking branch 'origin/master' into fix/blobpool
This commit is contained in:
commit
f28a4db105
15 changed files with 551 additions and 50 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
|
@ -19,7 +19,7 @@ eth/tracers/ @s1na
|
||||||
ethclient/ @fjl
|
ethclient/ @fjl
|
||||||
ethdb/ @rjl493456442
|
ethdb/ @rjl493456442
|
||||||
event/ @fjl
|
event/ @fjl
|
||||||
trie/ @rjl493456442
|
trie/ @rjl493456442 @gballet
|
||||||
triedb/ @rjl493456442
|
triedb/ @rjl493456442
|
||||||
core/tracing/ @s1na
|
core/tracing/ @s1na
|
||||||
graphql/ @s1na
|
graphql/ @s1na
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrDeleteRangeInterrupted = errors.New("safe delete range operation interrupted")
|
var ErrDeleteRangeInterrupted = errors.New("safe delete range operation interrupted")
|
||||||
|
|
@ -582,7 +581,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
||||||
}
|
}
|
||||||
total += ancient.size()
|
total += ancient.size()
|
||||||
}
|
}
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
table := newTableWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
|
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
|
||||||
table.SetFooter([]string{"", "Total", total.String(), " "})
|
table.SetFooter([]string{"", "Total", total.String(), " "})
|
||||||
table.AppendBulk(stats)
|
table.AppendBulk(stats)
|
||||||
|
|
|
||||||
208
core/rawdb/database_tablewriter_tinygo.go
Normal file
208
core/rawdb/database_tablewriter_tinygo.go
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
// TODO: naive stub implementation for tablewriter
|
||||||
|
|
||||||
|
//go:build tinygo
|
||||||
|
// +build tinygo
|
||||||
|
|
||||||
|
package rawdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cellPadding = 1 // Number of spaces on each side of cell content
|
||||||
|
totalPadding = 2 * cellPadding // Total padding per cell. Its two because we pad equally on both sides
|
||||||
|
)
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
out io.Writer
|
||||||
|
headers []string
|
||||||
|
footer []string
|
||||||
|
rows [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTableWriter(w io.Writer) *Table {
|
||||||
|
return &Table{out: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader sets the header row for the table. Headers define the column names
|
||||||
|
// and determine the number of columns for the entire table.
|
||||||
|
//
|
||||||
|
// All data rows and footer must have the same number of columns as the headers.
|
||||||
|
//
|
||||||
|
// Note: Headers are required - tables without headers will fail validation.
|
||||||
|
func (t *Table) SetHeader(headers []string) {
|
||||||
|
t.headers = headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFooter sets an optional footer row for the table.
|
||||||
|
//
|
||||||
|
// The footer must have the same number of columns as the headers, or validation will fail.
|
||||||
|
func (t *Table) SetFooter(footer []string) {
|
||||||
|
t.footer = footer
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBulk sets all data rows for the table at once, replacing any existing rows.
|
||||||
|
//
|
||||||
|
// Each row must have the same number of columns as the headers, or validation
|
||||||
|
// will fail during Render().
|
||||||
|
func (t *Table) AppendBulk(rows [][]string) {
|
||||||
|
t.rows = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render outputs the complete table to the configured writer. The table is rendered
|
||||||
|
// with headers, data rows, and optional footer.
|
||||||
|
//
|
||||||
|
// If validation fails, an error message is written to the output and rendering stops.
|
||||||
|
func (t *Table) Render() {
|
||||||
|
if err := t.render(); err != nil {
|
||||||
|
fmt.Fprintf(t.out, "Error: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Table) render() error {
|
||||||
|
if err := t.validateColumnCount(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
widths := t.calculateColumnWidths()
|
||||||
|
rowSeparator := t.buildRowSeparator(widths)
|
||||||
|
|
||||||
|
if len(t.headers) > 0 {
|
||||||
|
t.printRow(t.headers, widths)
|
||||||
|
fmt.Fprintln(t.out, rowSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range t.rows {
|
||||||
|
t.printRow(row, widths)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.footer) > 0 {
|
||||||
|
fmt.Fprintln(t.out, rowSeparator)
|
||||||
|
t.printRow(t.footer, widths)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateColumnCount checks that all rows and footer match the header column count
|
||||||
|
func (t *Table) validateColumnCount() error {
|
||||||
|
if len(t.headers) == 0 {
|
||||||
|
return errors.New("table must have headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCols := len(t.headers)
|
||||||
|
|
||||||
|
// Check all rows have same column count as headers
|
||||||
|
for i, row := range t.rows {
|
||||||
|
if len(row) != expectedCols {
|
||||||
|
return fmt.Errorf("row %d has %d columns, expected %d", i, len(row), expectedCols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check footer has same column count as headers (if present)
|
||||||
|
footerPresent := len(t.footer) > 0
|
||||||
|
if footerPresent && len(t.footer) != expectedCols {
|
||||||
|
return fmt.Errorf("footer has %d columns, expected %d", len(t.footer), expectedCols)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateColumnWidths determines the minimum width needed for each column.
|
||||||
|
//
|
||||||
|
// This is done by finding the longest content in each column across headers, rows, and footer.
|
||||||
|
//
|
||||||
|
// Returns an int slice where widths[i] is the display width for column i (including padding).
|
||||||
|
func (t *Table) calculateColumnWidths() []int {
|
||||||
|
// Headers define the number of columns
|
||||||
|
cols := len(t.headers)
|
||||||
|
if cols == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track maximum content width for each column (before padding)
|
||||||
|
widths := make([]int, cols)
|
||||||
|
|
||||||
|
// Start with header widths
|
||||||
|
for i, h := range t.headers {
|
||||||
|
widths[i] = len(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find max width needed for data cells in each column
|
||||||
|
for _, row := range t.rows {
|
||||||
|
for i, cell := range row {
|
||||||
|
widths[i] = max(widths[i], len(cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find max width needed for footer in each column
|
||||||
|
for i, f := range t.footer {
|
||||||
|
widths[i] = max(widths[i], len(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range widths {
|
||||||
|
widths[i] += totalPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRowSeparator creates a horizontal line to separate table rows.
|
||||||
|
//
|
||||||
|
// It generates a string with dashes (-) for each column width, joined by plus signs (+).
|
||||||
|
//
|
||||||
|
// Example output: "----------+--------+-----------"
|
||||||
|
func (t *Table) buildRowSeparator(widths []int) string {
|
||||||
|
parts := make([]string, len(widths))
|
||||||
|
for i, w := range widths {
|
||||||
|
parts[i] = strings.Repeat("-", w)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printRow outputs a single row to the table writer.
|
||||||
|
//
|
||||||
|
// Each cell is padded with spaces and separated by pipe characters (|).
|
||||||
|
//
|
||||||
|
// Example output: " Database | Size | Items "
|
||||||
|
func (t *Table) printRow(row []string, widths []int) {
|
||||||
|
for i, cell := range row {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprint(t.out, "|")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate centering pad without padding
|
||||||
|
contentWidth := widths[i] - totalPadding
|
||||||
|
cellLen := len(cell)
|
||||||
|
leftPadCentering := (contentWidth - cellLen) / 2
|
||||||
|
rightPadCentering := contentWidth - cellLen - leftPadCentering
|
||||||
|
|
||||||
|
// Build padded cell with centering
|
||||||
|
leftPadding := strings.Repeat(" ", cellPadding+leftPadCentering)
|
||||||
|
rightPadding := strings.Repeat(" ", cellPadding+rightPadCentering)
|
||||||
|
|
||||||
|
fmt.Fprintf(t.out, "%s%s%s", leftPadding, cell, rightPadding)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(t.out)
|
||||||
|
}
|
||||||
124
core/rawdb/database_tablewriter_tinygo_test.go
Normal file
124
core/rawdb/database_tablewriter_tinygo_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
//go:build tinygo
|
||||||
|
// +build tinygo
|
||||||
|
|
||||||
|
package rawdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableWriterTinyGo(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := newTableWriter(&buf)
|
||||||
|
|
||||||
|
headers := []string{"Database", "Size", "Items", "Status"}
|
||||||
|
rows := [][]string{
|
||||||
|
{"chaindata", "2.5 GB", "1,234,567", "Active"},
|
||||||
|
{"state", "890 MB", "456,789", "Active"},
|
||||||
|
{"ancient", "15.2 GB", "2,345,678", "Readonly"},
|
||||||
|
{"logs", "120 MB", "89,012", "Active"},
|
||||||
|
}
|
||||||
|
footer := []string{"Total", "18.71 GB", "4,125,046", "-"}
|
||||||
|
|
||||||
|
table.SetHeader(headers)
|
||||||
|
table.AppendBulk(rows)
|
||||||
|
table.SetFooter(footer)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
t.Logf("Table output using custom stub implementation:\n%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableWriterValidationErrors(t *testing.T) {
|
||||||
|
// Test missing headers
|
||||||
|
t.Run("MissingHeaders", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := newTableWriter(&buf)
|
||||||
|
|
||||||
|
rows := [][]string{{"x", "y", "z"}}
|
||||||
|
|
||||||
|
table.AppendBulk(rows)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "table must have headers") {
|
||||||
|
t.Errorf("Expected error for missing headers, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NotEnoughRowColumns", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := newTableWriter(&buf)
|
||||||
|
|
||||||
|
headers := []string{"A", "B", "C"}
|
||||||
|
badRows := [][]string{
|
||||||
|
{"x", "y"}, // Missing column
|
||||||
|
}
|
||||||
|
|
||||||
|
table.SetHeader(headers)
|
||||||
|
table.AppendBulk(badRows)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "row 0 has 2 columns, expected 3") {
|
||||||
|
t.Errorf("Expected validation error for row 0, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TooManyRowColumns", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := newTableWriter(&buf)
|
||||||
|
|
||||||
|
headers := []string{"A", "B", "C"}
|
||||||
|
badRows := [][]string{
|
||||||
|
{"p", "q", "r", "s"}, // Extra column
|
||||||
|
}
|
||||||
|
|
||||||
|
table.SetHeader(headers)
|
||||||
|
table.AppendBulk(badRows)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "row 0 has 4 columns, expected 3") {
|
||||||
|
t.Errorf("Expected validation error for row 0, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test mismatched footer columns
|
||||||
|
t.Run("MismatchedFooterColumns", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
table := newTableWriter(&buf)
|
||||||
|
|
||||||
|
headers := []string{"A", "B", "C"}
|
||||||
|
rows := [][]string{{"x", "y", "z"}}
|
||||||
|
footer := []string{"total", "sum"} // Missing column
|
||||||
|
|
||||||
|
table.SetHeader(headers)
|
||||||
|
table.AppendBulk(rows)
|
||||||
|
table.SetFooter(footer)
|
||||||
|
table.Render()
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "footer has 2 columns, expected 3") {
|
||||||
|
t.Errorf("Expected validation error for footer, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
33
core/rawdb/database_tablewriter_unix.go
Normal file
33
core/rawdb/database_tablewriter_unix.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
//go:build !tinygo
|
||||||
|
// +build !tinygo
|
||||||
|
|
||||||
|
package rawdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Re-export the real tablewriter types and functions
|
||||||
|
type Table = tablewriter.Table
|
||||||
|
|
||||||
|
func newTableWriter(w io.Writer) *Table {
|
||||||
|
return tablewriter.NewWriter(w)
|
||||||
|
}
|
||||||
|
|
@ -384,21 +384,48 @@ func accountHistoryIndexKey(addressHash common.Hash) []byte {
|
||||||
|
|
||||||
// storageHistoryIndexKey = StateHistoryStorageMetadataPrefix + addressHash + storageHash
|
// storageHistoryIndexKey = StateHistoryStorageMetadataPrefix + addressHash + storageHash
|
||||||
func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []byte {
|
func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []byte {
|
||||||
return append(append(StateHistoryStorageMetadataPrefix, addressHash.Bytes()...), storageHash.Bytes()...)
|
totalLen := len(StateHistoryStorageMetadataPrefix) + 2*common.HashLength
|
||||||
|
out := make([]byte, totalLen)
|
||||||
|
|
||||||
|
off := 0
|
||||||
|
off += copy(out[off:], StateHistoryStorageMetadataPrefix)
|
||||||
|
off += copy(out[off:], addressHash.Bytes())
|
||||||
|
copy(out[off:], storageHash.Bytes())
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
|
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
|
||||||
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
|
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
|
||||||
var buf [4]byte
|
var buf4 [4]byte
|
||||||
binary.BigEndian.PutUint32(buf[:], blockID)
|
binary.BigEndian.PutUint32(buf4[:], blockID)
|
||||||
return append(append(StateHistoryAccountBlockPrefix, addressHash.Bytes()...), buf[:]...)
|
|
||||||
|
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[:])
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
|
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
|
||||||
func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
|
func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
|
||||||
var buf [4]byte
|
var buf4 [4]byte
|
||||||
binary.BigEndian.PutUint32(buf[:], blockID)
|
binary.BigEndian.PutUint32(buf4[:], blockID)
|
||||||
return append(append(append(StateHistoryStorageBlockPrefix, addressHash.Bytes()...), storageHash.Bytes()...), buf[:]...)
|
|
||||||
|
totalLen := len(StateHistoryStorageBlockPrefix) + 2*common.HashLength + 4
|
||||||
|
out := make([]byte, totalLen)
|
||||||
|
|
||||||
|
off := 0
|
||||||
|
off += copy(out[off:], StateHistoryStorageBlockPrefix)
|
||||||
|
off += copy(out[off:], addressHash.Bytes())
|
||||||
|
off += copy(out[off:], storageHash.Bytes())
|
||||||
|
copy(out[off:], buf4[:])
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// transitionStateKey = transitionStatusKey + hash
|
// transitionStateKey = transitionStatusKey + hash
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,12 @@ package catalyst
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/beacon/engine"
|
"github.com/ethereum/go-ethereum/beacon/engine"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -80,41 +82,6 @@ const (
|
||||||
beaconUpdateWarnFrequency = 5 * time.Minute
|
beaconUpdateWarnFrequency = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// All methods provided over the engine endpoint.
|
|
||||||
var caps = []string{
|
|
||||||
"engine_forkchoiceUpdatedV1",
|
|
||||||
"engine_forkchoiceUpdatedV2",
|
|
||||||
"engine_forkchoiceUpdatedV3",
|
|
||||||
"engine_forkchoiceUpdatedWithWitnessV1",
|
|
||||||
"engine_forkchoiceUpdatedWithWitnessV2",
|
|
||||||
"engine_forkchoiceUpdatedWithWitnessV3",
|
|
||||||
"engine_exchangeTransitionConfigurationV1",
|
|
||||||
"engine_getPayloadV1",
|
|
||||||
"engine_getPayloadV2",
|
|
||||||
"engine_getPayloadV3",
|
|
||||||
"engine_getPayloadV4",
|
|
||||||
"engine_getPayloadV5",
|
|
||||||
"engine_getBlobsV1",
|
|
||||||
"engine_getBlobsV2",
|
|
||||||
"engine_newPayloadV1",
|
|
||||||
"engine_newPayloadV2",
|
|
||||||
"engine_newPayloadV3",
|
|
||||||
"engine_newPayloadV4",
|
|
||||||
"engine_newPayloadWithWitnessV1",
|
|
||||||
"engine_newPayloadWithWitnessV2",
|
|
||||||
"engine_newPayloadWithWitnessV3",
|
|
||||||
"engine_newPayloadWithWitnessV4",
|
|
||||||
"engine_executeStatelessPayloadV1",
|
|
||||||
"engine_executeStatelessPayloadV2",
|
|
||||||
"engine_executeStatelessPayloadV3",
|
|
||||||
"engine_executeStatelessPayloadV4",
|
|
||||||
"engine_getPayloadBodiesByHashV1",
|
|
||||||
"engine_getPayloadBodiesByHashV2",
|
|
||||||
"engine_getPayloadBodiesByRangeV1",
|
|
||||||
"engine_getPayloadBodiesByRangeV2",
|
|
||||||
"engine_getClientVersionV1",
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Number of blobs requested via getBlobsV2
|
// Number of blobs requested via getBlobsV2
|
||||||
getBlobsRequestedCounter = metrics.NewRegisteredCounter("engine/getblobs/requested", nil)
|
getBlobsRequestedCounter = metrics.NewRegisteredCounter("engine/getblobs/requested", nil)
|
||||||
|
|
@ -916,6 +883,15 @@ func (api *ConsensusAPI) checkFork(timestamp uint64, forks ...forks.Fork) bool {
|
||||||
|
|
||||||
// ExchangeCapabilities returns the current methods provided by this node.
|
// ExchangeCapabilities returns the current methods provided by this node.
|
||||||
func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
|
func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
|
||||||
|
valueT := reflect.TypeOf(api)
|
||||||
|
caps := make([]string, 0, valueT.NumMethod())
|
||||||
|
for i := 0; i < valueT.NumMethod(); i++ {
|
||||||
|
name := []rune(valueT.Method(i).Name)
|
||||||
|
if string(name) == "ExchangeCapabilities" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
caps = append(caps, "engine_"+string(unicode.ToLower(name[0]))+string(name[1:]))
|
||||||
|
}
|
||||||
return caps
|
return caps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ func (s *Syncer) run() {
|
||||||
target *types.Header
|
target *types.Header
|
||||||
ticker = time.NewTicker(time.Second * 5)
|
ticker = time.NewTicker(time.Second * 5)
|
||||||
)
|
)
|
||||||
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case req := <-s.request:
|
case req := <-s.request:
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ func (ec *Client) SubscribePendingTransactions(ctx context.Context, ch chan<- co
|
||||||
// and returns them as a JSON object.
|
// and returns them as a JSON object.
|
||||||
func (ec *Client) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (any, error) {
|
func (ec *Client) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (any, error) {
|
||||||
var result any
|
var result any
|
||||||
err := ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash.Hex(), config)
|
err := ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,40 @@ func TestWithdrawals(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGraphQLMaxDepth ensures that queries exceeding the configured maximum depth
|
||||||
|
// are rejected to prevent resource exhaustion from deeply nested operations.
|
||||||
|
func TestGraphQLMaxDepth(t *testing.T) {
|
||||||
|
stack := createNode(t)
|
||||||
|
defer stack.Close()
|
||||||
|
|
||||||
|
h, err := newHandler(stack, nil, nil, []string{}, []string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create graphql service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i := 0; i < maxQueryDepth+1; i++ {
|
||||||
|
b.WriteString("ommers{")
|
||||||
|
}
|
||||||
|
b.WriteString("number")
|
||||||
|
for i := 0; i < maxQueryDepth+1; i++ {
|
||||||
|
b.WriteString("}")
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf("{block{%s}}", b.String())
|
||||||
|
|
||||||
|
res := h.Schema.Exec(context.Background(), query, "", nil)
|
||||||
|
var found bool
|
||||||
|
for _, err := range res.Errors {
|
||||||
|
if err.Rule == "MaxDepthExceeded" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected max depth exceeded error, got %v", res.Errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createNode(t *testing.T) *node.Node {
|
func createNode(t *testing.T) *node.Node {
|
||||||
stack, err := node.New(&node.Config{
|
stack, err := node.New(&node.Config{
|
||||||
HTTPHost: "127.0.0.1",
|
HTTPHost: "127.0.0.1",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import (
|
||||||
gqlErrors "github.com/graph-gophers/graphql-go/errors"
|
gqlErrors "github.com/graph-gophers/graphql-go/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxQueryDepth limits the maximum field nesting depth allowed in GraphQL queries.
|
||||||
|
const maxQueryDepth = 20
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
Schema *graphql.Schema
|
Schema *graphql.Schema
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +119,7 @@ func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterS
|
||||||
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
|
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
|
||||||
q := Resolver{backend, filterSystem}
|
q := Resolver{backend, filterSystem}
|
||||||
|
|
||||||
s, err := graphql.ParseSchema(schema, &q)
|
s, err := graphql.ParseSchema(schema, &q, graphql.MaxDepth(maxQueryDepth))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,7 @@ type Config struct {
|
||||||
// This field must be set to a valid secp256k1 private key.
|
// This field must be set to a valid secp256k1 private key.
|
||||||
PrivateKey *ecdsa.PrivateKey `toml:"-"`
|
PrivateKey *ecdsa.PrivateKey `toml:"-"`
|
||||||
|
|
||||||
// MaxPeers is the maximum number of peers that can be
|
// MaxPeers is the maximum number of peers that can be connected.
|
||||||
// connected. It must be greater than zero.
|
|
||||||
MaxPeers int
|
MaxPeers int
|
||||||
|
|
||||||
// MaxPendingPeers is the maximum number of peers that can be pending in the
|
// MaxPendingPeers is the maximum number of peers that can be pending in the
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ type Server struct {
|
||||||
batchItemLimit int
|
batchItemLimit int
|
||||||
batchResponseLimit int
|
batchResponseLimit int
|
||||||
httpBodyLimit int
|
httpBodyLimit int
|
||||||
|
wsReadLimit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new server instance with no registered handlers.
|
// NewServer creates a new server instance with no registered handlers.
|
||||||
|
|
@ -62,6 +63,7 @@ func NewServer() *Server {
|
||||||
idgen: randomIDGenerator(),
|
idgen: randomIDGenerator(),
|
||||||
codecs: make(map[ServerCodec]struct{}),
|
codecs: make(map[ServerCodec]struct{}),
|
||||||
httpBodyLimit: defaultBodyLimit,
|
httpBodyLimit: defaultBodyLimit,
|
||||||
|
wsReadLimit: wsDefaultReadLimit,
|
||||||
}
|
}
|
||||||
server.run.Store(true)
|
server.run.Store(true)
|
||||||
// Register the default service providing meta information about the RPC service such
|
// Register the default service providing meta information about the RPC service such
|
||||||
|
|
@ -89,6 +91,13 @@ func (s *Server) SetHTTPBodyLimit(limit int) {
|
||||||
s.httpBodyLimit = limit
|
s.httpBodyLimit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWebsocketReadLimit sets the limit for max message size for Websocket requests.
|
||||||
|
//
|
||||||
|
// This method should be called before processing any requests via Websocket server.
|
||||||
|
func (s *Server) SetWebsocketReadLimit(limit int64) {
|
||||||
|
s.wsReadLimit = limit
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterName creates a service for the given receiver type under the given name. When no
|
// RegisterName creates a service for the given receiver type under the given name. When no
|
||||||
// methods on the given receiver match the criteria to be either an RPC method or a
|
// methods on the given receiver match the criteria to be either an RPC method or a
|
||||||
// subscription an error is returned. Otherwise a new service is created and added to the
|
// subscription an error is returned. Otherwise a new service is created and added to the
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,18 @@ package rpc
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerRegisterName(t *testing.T) {
|
func TestServerRegisterName(t *testing.T) {
|
||||||
|
|
@ -202,3 +207,86 @@ func TestServerBatchResponseSizeLimit(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerWebsocketReadLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Test different read limits
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
readLimit int64
|
||||||
|
testSize int
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "limit with small request - should succeed",
|
||||||
|
readLimit: 4096, // generous limit to comfortably allow JSON overhead
|
||||||
|
testSize: 256, // reasonably small payload
|
||||||
|
shouldFail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "limit with large request - should fail",
|
||||||
|
readLimit: 256, // tight limit to trigger server-side read limit
|
||||||
|
testSize: 1024, // payload that will exceed the limit including JSON overhead
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Create server and set read limits
|
||||||
|
srv := newTestServer()
|
||||||
|
srv.SetWebsocketReadLimit(tc.readLimit)
|
||||||
|
defer srv.Stop()
|
||||||
|
|
||||||
|
// Start HTTP server with WebSocket handler
|
||||||
|
httpsrv := httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
|
||||||
|
defer httpsrv.Close()
|
||||||
|
|
||||||
|
wsURL := "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
|
||||||
|
|
||||||
|
// Connect WebSocket client
|
||||||
|
client, err := DialOptions(context.Background(), wsURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't dial: %v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Create large request data - this is what will be limited
|
||||||
|
largeString := strings.Repeat("A", tc.testSize)
|
||||||
|
|
||||||
|
// Send the large string as a parameter in the request
|
||||||
|
var result echoResult
|
||||||
|
err = client.Call(&result, "test_echo", largeString, 42, &echoArgs{S: "test"})
|
||||||
|
|
||||||
|
if tc.shouldFail {
|
||||||
|
// Expecting an error due to read limit exceeded
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for request size %d with limit %d, but got none", tc.testSize, tc.readLimit)
|
||||||
|
}
|
||||||
|
// Be tolerant about the exact error surfaced by gorilla/websocket.
|
||||||
|
// Prefer a CloseError with code 1009, but accept ErrReadLimit or an error string containing 1009/message too big.
|
||||||
|
var cerr *websocket.CloseError
|
||||||
|
if errors.As(err, &cerr) {
|
||||||
|
if cerr.Code != websocket.CloseMessageTooBig {
|
||||||
|
t.Fatalf("unexpected websocket close code: have %d want %d (err=%v)", cerr.Code, websocket.CloseMessageTooBig, err)
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, websocket.ErrReadLimit) &&
|
||||||
|
!strings.Contains(strings.ToLower(err.Error()), "1009") &&
|
||||||
|
!strings.Contains(strings.ToLower(err.Error()), "message too big") {
|
||||||
|
// Not the error we expect from exceeding the message size limit.
|
||||||
|
t.Fatalf("unexpected error for read limit violation: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expecting success
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error for request size %d with limit %d: %v", tc.testSize, tc.readLimit, err)
|
||||||
|
}
|
||||||
|
// Verify the response is correct - the echo should return our string
|
||||||
|
if result.String != largeString {
|
||||||
|
t.Fatalf("expected echo result to match input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
|
||||||
log.Debug("WebSocket upgrade failed", "err", err)
|
log.Debug("WebSocket upgrade failed", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
codec := newWebsocketCodec(conn, r.Host, r.Header, wsDefaultReadLimit)
|
codec := newWebsocketCodec(conn, r.Host, r.Header, s.wsReadLimit)
|
||||||
s.ServeCodec(codec, 0)
|
s.ServeCodec(codec, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue