diff --git a/client.go b/client.go index b906b43..631270f 100644 --- a/client.go +++ b/client.go @@ -2,11 +2,12 @@ package main import ( "context" - "crypto/rand" "fmt" "os" "os/exec" "path/filepath" + "strings" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -25,6 +26,12 @@ type Client interface { // HttpAddr returns the address where the client is serving JSON-RPC. HttpAddr() string + // AuthAddr returns the address where the client is serving authenticated JSON-RPC. + AuthAddr() string + + // JWTSecret returns the JWT secret for authenticated endpoints. + JWTSecret() []byte + // Close closes the client. Close() error } @@ -84,13 +91,7 @@ func newGethClient(ctx context.Context, geth string, chaindir string, verbose bo return nil, err } - jwt := make([]byte, 32) - rand.Read(jwt) - if err := os.WriteFile(fmt.Sprintf("%s/jwt.hex", tmp), []byte(hexutil.Encode(jwt)), 0600); err != nil { - return nil, err - } - - g := &gethClient{path: geth, workdir: tmp, jwt: jwt, fcu: fcuRequest} + g := &gethClient{path: geth, workdir: tmp, jwt: nil, fcu: fcuRequest} return g, nil } @@ -107,11 +108,12 @@ func (g *gethClient) Start(ctx context.Context, verbose bool) error { "--gcmode=archive", "--nodiscover", "--http", - "--http.api=admin,eth,debug,net", + "--http.api=admin,eth,debug,net,testing", fmt.Sprintf("--http.addr=%s", HOST), fmt.Sprintf("--http.port=%s", PORT), + fmt.Sprintf("--authrpc.addr=%s", HOST), fmt.Sprintf("--authrpc.port=%s", AUTHPORT), - fmt.Sprintf("--authrpc.jwtsecret=%s", fmt.Sprintf("%s/jwt.hex", g.workdir)), + // authrpc needed for engine API (forkchoiceUpdated) } ) g.cmd = exec.CommandContext(ctx, g.path, options...) @@ -128,6 +130,29 @@ func (g *gethClient) Start(ctx context.Context, verbose bool) error { // AfterStart is called after the client has been fully started. // We send a forkchoiceUpdatedV2 request to the engine to trigger a post-merge forkchoice. func (g *gethClient) AfterStart(ctx context.Context) error { + // Read JWT secret from the file that geth generated + // Wait for geth to create the file (it generates it on startup) + jwtPath := fmt.Sprintf("%s/geth/jwtsecret", g.workdir) + var jwtBytes []byte + var err error + for i := 0; i < 10; i++ { + jwtBytes, err = os.ReadFile(jwtPath) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + if err != nil { + return fmt.Errorf("failed to read JWT secret: %w", err) + } + // JWT secret file contains hex-encoded bytes, one per line + jwtHex := strings.TrimSpace(string(jwtBytes)) + jwt, err := hexutil.Decode(jwtHex) + if err != nil { + return fmt.Errorf("failed to decode JWT secret: %w", err) + } + g.jwt = jwt + auth := node.NewJWTAuth(common.BytesToHash(g.jwt)) endpoint := fmt.Sprintf("http://%s:%s", HOST, AUTHPORT) cl, err := rpc.DialOptions(ctx, endpoint, rpc.WithHTTPAuth(auth)) @@ -147,6 +172,30 @@ func (g *gethClient) HttpAddr() string { return fmt.Sprintf("http://%s:%s", HOST, PORT) } +// AuthAddr returns the address where the client is serving authenticated JSON-RPC. +func (g *gethClient) AuthAddr() string { + return fmt.Sprintf("http://%s:%s", HOST, AUTHPORT) +} + +// JWTSecret returns the JWT secret for authenticated endpoints. +func (g *gethClient) JWTSecret() []byte { + // If JWT hasn't been loaded yet, read it from the file + if g.jwt == nil { + jwtPath := fmt.Sprintf("%s/geth/jwtsecret", g.workdir) + jwtBytes, err := os.ReadFile(jwtPath) + if err != nil { + return nil + } + jwtHex := strings.TrimSpace(string(jwtBytes)) + jwt, err := hexutil.Decode(jwtHex) + if err != nil { + return nil + } + g.jwt = jwt + } + return g.jwt +} + // Close closes the client. func (g *gethClient) Close() error { g.cmd.Process.Kill() diff --git a/ethclient.go b/ethclient.go index 82f63ca..9246909 100644 --- a/ethclient.go +++ b/ethclient.go @@ -9,6 +9,8 @@ import ( "os" "strings" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" ) @@ -35,6 +37,24 @@ func newEthclientHandler(addr string) (*ethclientHandler, error) { }, nil } +func newAuthEthclientHandler(addr string, jwtSecret []byte) (*ethclientHandler, error) { + rt := &loggingRoundTrip{ + inner: http.DefaultTransport, + } + httpClient := rpc.WithHTTPClient(&http.Client{Transport: rt}) + auth := node.NewJWTAuth(common.BytesToHash(jwtSecret)) + ctx := context.Background() + rpcClient, err := rpc.DialOptions(ctx, addr, httpClient, rpc.WithHTTPAuth(auth)) + if err != nil { + return nil, err + } + return ðclientHandler{ + rpc: rpcClient, + logFile: nil, + transport: rt, + }, nil +} + func (l *ethclientHandler) RotateLog(filename string) error { if l.logFile != nil { if err := l.logFile.Close(); err != nil { diff --git a/generate.go b/generate.go index a5f89fd..7d24938 100644 --- a/generate.go +++ b/generate.go @@ -87,6 +87,7 @@ func runGenerator(ctx context.Context) error { fmt.Println(" fail.") fmt.Fprintf(os.Stderr, "failed to fill %s/%s: %s\n", methodTest.Name, test.Name, err) fails++ + handler.Close() continue } fmt.Println(" done.") diff --git a/go.mod b/go.mod index 8124fa6..5983831 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 ) +replace github.com/ethereum/go-ethereum => github.com/MariusVanDerWijden/go-ethereum v1.8.22-0.20260122150205-ca4e4aee07f2 + require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -31,7 +33,6 @@ require ( github.com/consensys/gnark-crypto v0.18.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // 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/davecgh/go-spew v1.1.1 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect @@ -43,12 +44,13 @@ require ( github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect - github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fatih/color v1.16.0 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gofrs/flock v0.12.1 // indirect @@ -56,9 +58,8 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/graph-gophers/graphql-go v1.3.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect @@ -69,7 +70,6 @@ require ( github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect - github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 // indirect github.com/kilic/bls12-381 v0.1.0 // indirect github.com/klauspost/compress v1.16.0 // indirect @@ -85,7 +85,6 @@ require ( github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/naoina/go-stringutil v0.1.0 // indirect github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opentracing/opentracing-go v1.1.0 // indirect github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect @@ -102,7 +101,7 @@ require ( github.com/protolambda/zrnt v0.34.1 // indirect github.com/protolambda/ztyp v0.2.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect @@ -113,11 +112,15 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/automaxprocs v1.5.2 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 4964ae7..152af69 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/MariusVanDerWijden/go-ethereum v1.8.22-0.20260122150205-ca4e4aee07f2 h1:ZQHr0kB3MU1Xre2205N47wcaiXA9uA4I/s7jrUy17Pg= +github.com/MariusVanDerWijden/go-ethereum v1.8.22-0.20260122150205-ca4e4aee07f2/go.mod h1:RRZTBfsoT4iNtjxzeYEzu3JWRZ8+Wq+aA5PmJygdEkQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= @@ -39,16 +41,12 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= 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/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -77,20 +75,10 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 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.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU= -github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= 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-ethereum v1.16.3-0.20251008092951-168d699fbad6 h1:25ayyf4xJDnmUMvrquTx3orqUZN0A4m1/PaFsrUGAUc= -github.com/ethereum/go-ethereum v1.16.3-0.20251008092951-168d699fbad6/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok= -github.com/ethereum/go-ethereum v1.16.3-0.20251128102831-a122dbe45993 h1:IdHG9k0piIUtAWlqiD+/qKE1sgiBqcMsfggUWbsiAZk= -github.com/ethereum/go-ethereum v1.16.3-0.20251128102831-a122dbe45993/go.mod h1:q9cnybcxWmytcJ6rMhLQSji1ZZ2i8TvoPNE+hXEu5bQ= -github.com/ethereum/go-ethereum v1.16.3-0.20251204100242-73a2df2b0a7d h1:o5qOWZlWQvK3MAv+W3rpDNw6F/uFmyUmRqtU+EghCpM= -github.com/ethereum/go-ethereum v1.16.3-0.20251204100242-73a2df2b0a7d/go.mod h1:q9cnybcxWmytcJ6rMhLQSji1ZZ2i8TvoPNE+hXEu5bQ= -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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= @@ -108,6 +96,11 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -138,14 +131,14 @@ github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGS github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -172,8 +165,6 @@ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= -github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 h1:msKODTL1m0wigztaqILOtla9HeW1ciscYG4xjLtvk5I= github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= @@ -215,7 +206,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -232,8 +222,6 @@ github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcou github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= @@ -287,14 +275,12 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= @@ -313,8 +299,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -333,6 +319,16 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -410,8 +406,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/testgen/generators.go b/testgen/generators.go index 4e7bdc3..b801ef5 100644 --- a/testgen/generators.go +++ b/testgen/generators.go @@ -91,6 +91,7 @@ var AllMethods = []MethodTests{ DebugGetRawTransaction, EthBlobBaseFee, NetVersion, + TestingBuildBlockV1, // -- gas price tests are disabled because of non-determinism // EthGasPrice, @@ -2271,6 +2272,691 @@ var NetVersion = MethodTests{ }, } +// TestingBuildBlockV1 stores a list of all tests against the method. +var TestingBuildBlockV1 = MethodTests{ + "testing_buildBlockV1", + []Test{ + { + Name: "build-block-with-transactions", + About: "builds a block with specified transactions using testing_buildBlockV1", + Run: func(ctx context.Context, t *T) error { + // Get the parent block (head of chain) + parentBlock := t.chain.Head() + parentHash := parentBlock.Hash() + + // Create payload attributes + // PayloadAttributesV3 structure based on the spec + payloadAttrs := map[string]interface{}{ + "timestamp": hexutil.Uint64(parentBlock.Time() + 12), // 12 seconds in the future + "prevRandao": common.Hash{}.Hex(), // zero hash + "suggestedFeeRecipient": common.Address{}.Hex(), // zero address + "withdrawals": []interface{}{}, + } + + // Add parentBeaconBlockRoot if Cancun is active + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + beaconRoot := common.Hash{0xcf, 0x8e, 0x0d, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x01, 0xd0, 0x79, 0x03, 0x47, 0x32, 0x03, 0x02, 0xcc, 0x09, 0x43, 0xd5, 0xa1, 0x88, 0x43, 0x65, 0x14, 0x9a, 0x42, 0x21, 0x2e, 0x88, 0x22} + payloadAttrs["parentBeaconBlockRoot"] = beaconRoot.Hex() + } + + // Create some transactions to include + sender, nonce := t.chain.GetSender(0) + basefee := parentBlock.BaseFee() + if basefee == nil { + basefee = big.NewInt(1000000000) // 1 gwei default + } + gasFeeCap := new(big.Int).Add(basefee, big.NewInt(500)) + + txdata := &types.DynamicFeeTx{ + Nonce: nonce, + To: &emitContract, + Gas: 21000, + GasTipCap: big.NewInt(500), + GasFeeCap: gasFeeCap, + Value: big.NewInt(1000), + } + tx := t.chain.MustSignTx(sender, txdata) + + // Marshal transaction to hex (raw signed transaction, same format as eth_sendRawTransaction) + // According to spec: "an array of raw, signed transactions (hex-encoded 0x... strings)" + txBytes, err := tx.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal transaction: %w", err) + } + txHex := hexutil.Encode(txBytes) + + // Prepare request parameters (4 separate params per spec) + extraData := hexutil.Encode([]byte("test_name")) + + // Call the RPC method with 4 separate parameters + // Per spec: transactions must be "an array of raw, signed transactions (hex-encoded 0x... strings)" + // We pass []string with hex-encoded strings - if implementation fails, it should error + var result map[string]interface{} + err = t.rpc.CallContext(ctx, &result, "testing_buildBlockV1", + parentHash.Hex(), // param 1: parentBlockHash + payloadAttrs, // param 2: payloadAttributes + []string{txHex}, // param 3: transactions (as hex strings per spec) + extraData, // param 4: extraData + ) + if err != nil { + return fmt.Errorf("testing_buildBlockV1 call failed: %w", err) + } + + // Validate response structure + executionPayload, ok := result["executionPayload"].(map[string]interface{}) + if !ok { + return fmt.Errorf("missing or invalid executionPayload in response") + } + + // Verify parent hash matches + if parentHashStr, ok := executionPayload["parentHash"].(string); ok { + if parentHashStr != parentHash.Hex() { + return fmt.Errorf("parentHash mismatch: got %s, want %s", parentHashStr, parentHash.Hex()) + } + } else { + return fmt.Errorf("missing parentHash in executionPayload") + } + + // Verify block number is parent + 1 + if blockNumStr, ok := executionPayload["blockNumber"].(string); ok { + blockNum := hexutil.MustDecodeUint64(blockNumStr) + expectedNum := parentBlock.NumberU64() + 1 + if blockNum != expectedNum { + return fmt.Errorf("blockNumber mismatch: got %d, want %d", blockNum, expectedNum) + } + } else { + return fmt.Errorf("missing blockNumber in executionPayload") + } + + // Spec: "The client MUST use the provided payloadAttributes" + // Verify timestamp matches payloadAttributes + if timestampStr, ok := executionPayload["timestamp"].(string); ok { + gotTimestamp := hexutil.MustDecodeUint64(timestampStr) + expectedTimestamp := uint64(payloadAttrs["timestamp"].(hexutil.Uint64)) + if gotTimestamp != expectedTimestamp { + return fmt.Errorf("timestamp mismatch: got %d, want %d", gotTimestamp, expectedTimestamp) + } + } else { + return fmt.Errorf("missing timestamp in executionPayload") + } + + // Verify prevRandao matches payloadAttributes + if prevRandaoStr, ok := executionPayload["prevRandao"].(string); ok { + expectedPrevRandao := common.HexToHash(payloadAttrs["prevRandao"].(string)) + gotPrevRandao := common.HexToHash(prevRandaoStr) + if gotPrevRandao != expectedPrevRandao { + return fmt.Errorf("prevRandao mismatch: got %s, want %s", gotPrevRandao.Hex(), expectedPrevRandao.Hex()) + } + } else { + return fmt.Errorf("missing prevRandao in executionPayload") + } + + // Verify feeRecipient matches suggestedFeeRecipient from payloadAttributes + if feeRecipientStr, ok := executionPayload["feeRecipient"].(string); ok { + expectedFeeRecipient := common.HexToAddress(payloadAttrs["suggestedFeeRecipient"].(string)) + gotFeeRecipient := common.HexToAddress(feeRecipientStr) + if gotFeeRecipient != expectedFeeRecipient { + return fmt.Errorf("feeRecipient mismatch: got %s, want %s", gotFeeRecipient.Hex(), expectedFeeRecipient.Hex()) + } + } else { + return fmt.Errorf("missing feeRecipient in executionPayload") + } + + // Verify parentBeaconBlockRoot if Cancun is active and it's present in response + // Note: Some implementations may not include this field depending on ExecutionPayload version + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + if payloadBeaconRoot, hasBeaconRoot := payloadAttrs["parentBeaconBlockRoot"]; hasBeaconRoot { + if beaconRootStr, ok := executionPayload["parentBeaconBlockRoot"].(string); ok { + expectedBeaconRoot := common.HexToHash(payloadBeaconRoot.(string)) + gotBeaconRoot := common.HexToHash(beaconRootStr) + if gotBeaconRoot != expectedBeaconRoot { + return fmt.Errorf("parentBeaconBlockRoot mismatch: got %s, want %s", gotBeaconRoot.Hex(), expectedBeaconRoot.Hex()) + } + } + // If parentBeaconBlockRoot was provided in payloadAttributes but missing in response, + // this may indicate the implementation is using an older ExecutionPayload version + } + } + + // Spec: "The client MUST include all transactions from the transactions array on the block's transaction list, in the order they were provided." + // Spec: "The client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the transactions array." + if txs, ok := executionPayload["transactions"].([]interface{}); ok { + if len(txs) != 1 { + return fmt.Errorf("expected exactly 1 transaction (only from transactions array), got %d", len(txs)) + } + if txStr, ok := txs[0].(string); ok { + expectedTxHex := hexutil.Encode(txBytes) + if txStr != expectedTxHex { + return fmt.Errorf("transaction mismatch: got %s, want %s", txStr, expectedTxHex) + } + } else { + return fmt.Errorf("transaction is not a string") + } + } else { + return fmt.Errorf("missing or invalid transactions in executionPayload") + } + + // Spec: "If extraData is provided, the client MUST set the extraData field of the resulting payload to this value." + if extraDataStr, ok := executionPayload["extraData"].(string); ok { + expectedExtraData := hexutil.Encode([]byte("test_name")) + if extraDataStr != expectedExtraData { + return fmt.Errorf("extraData mismatch: got %s, want %s (per spec, must match provided value)", extraDataStr, expectedExtraData) + } + } else { + return fmt.Errorf("missing extraData in executionPayload") + } + + // Spec: "This method MUST NOT modify the client's canonical chain or head block." + // Verify chain head hasn't changed + newHead := t.chain.Head() + if newHead.Hash() != parentHash { + return fmt.Errorf("chain head changed (method should be read-only): was %s, now %s", parentHash.Hex(), newHead.Hash().Hex()) + } + + return nil + }, + }, + { + Name: "build-block-no-extra-data", + About: "builds a block with null extraData using testing_buildBlockV1", + Run: func(ctx context.Context, t *T) error { + // Get the parent block (head of chain) + parentBlock := t.chain.Head() + parentHash := parentBlock.Hash() + + // Create payload attributes + payloadAttrs := map[string]interface{}{ + "timestamp": hexutil.Uint64(parentBlock.Time() + 12), + "prevRandao": common.Hash{}.Hex(), + "suggestedFeeRecipient": common.Address{}.Hex(), + "withdrawals": []interface{}{}, + } + + // Add parentBeaconBlockRoot if Cancun is active + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + beaconRoot := common.Hash{0xcf, 0x8e, 0x0d, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x01, 0xd0, 0x79, 0x03, 0x47, 0x32, 0x03, 0x02, 0xcc, 0x09, 0x43, 0xd5, 0xa1, 0x88, 0x43, 0x65, 0x14, 0x9a, 0x42, 0x21, 0x2e, 0x88, 0x22} + payloadAttrs["parentBeaconBlockRoot"] = beaconRoot.Hex() + } + + // Call the RPC method with 4 separate parameters (extraData is null) + var result map[string]interface{} + err := t.rpc.CallContext(ctx, &result, "testing_buildBlockV1", + parentHash.Hex(), // param 1: parentBlockHash + payloadAttrs, // param 2: payloadAttributes + []string{}, // param 3: transactions + nil, // param 4: extraData (null) + ) + if err != nil { + return fmt.Errorf("testing_buildBlockV1 call failed: %w", err) + } + + // Validate response structure + executionPayload, ok := result["executionPayload"].(map[string]interface{}) + if !ok { + return fmt.Errorf("missing or invalid executionPayload in response") + } + + // Verify parent hash matches + if parentHashStr, ok := executionPayload["parentHash"].(string); ok { + if parentHashStr != parentHash.Hex() { + return fmt.Errorf("parentHash mismatch: got %s, want %s", parentHashStr, parentHash.Hex()) + } + } else { + return fmt.Errorf("missing parentHash in executionPayload") + } + + // Verify block number is parent + 1 + if blockNumStr, ok := executionPayload["blockNumber"].(string); ok { + blockNum := hexutil.MustDecodeUint64(blockNumStr) + expectedNum := parentBlock.NumberU64() + 1 + if blockNum != expectedNum { + return fmt.Errorf("blockNumber mismatch: got %d, want %d", blockNum, expectedNum) + } + } else { + return fmt.Errorf("missing blockNumber in executionPayload") + } + + // Spec: "The client MUST use the provided payloadAttributes" + // Verify timestamp matches payloadAttributes + if timestampStr, ok := executionPayload["timestamp"].(string); ok { + gotTimestamp := hexutil.MustDecodeUint64(timestampStr) + expectedTimestamp := uint64(payloadAttrs["timestamp"].(hexutil.Uint64)) + if gotTimestamp != expectedTimestamp { + return fmt.Errorf("timestamp mismatch: got %d, want %d", gotTimestamp, expectedTimestamp) + } + } else { + return fmt.Errorf("missing timestamp in executionPayload") + } + + // Verify prevRandao matches payloadAttributes + if prevRandaoStr, ok := executionPayload["prevRandao"].(string); ok { + expectedPrevRandao := common.HexToHash(payloadAttrs["prevRandao"].(string)) + gotPrevRandao := common.HexToHash(prevRandaoStr) + if gotPrevRandao != expectedPrevRandao { + return fmt.Errorf("prevRandao mismatch: got %s, want %s", gotPrevRandao.Hex(), expectedPrevRandao.Hex()) + } + } else { + return fmt.Errorf("missing prevRandao in executionPayload") + } + + // Verify feeRecipient matches suggestedFeeRecipient from payloadAttributes + if feeRecipientStr, ok := executionPayload["feeRecipient"].(string); ok { + expectedFeeRecipient := common.HexToAddress(payloadAttrs["suggestedFeeRecipient"].(string)) + gotFeeRecipient := common.HexToAddress(feeRecipientStr) + if gotFeeRecipient != expectedFeeRecipient { + return fmt.Errorf("feeRecipient mismatch: got %s, want %s", gotFeeRecipient.Hex(), expectedFeeRecipient.Hex()) + } + } else { + return fmt.Errorf("missing feeRecipient in executionPayload") + } + + // Verify parentBeaconBlockRoot if Cancun is active and it's present in response + // Note: Some implementations may not include this field depending on ExecutionPayload version + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + if payloadBeaconRoot, hasBeaconRoot := payloadAttrs["parentBeaconBlockRoot"]; hasBeaconRoot { + if beaconRootStr, ok := executionPayload["parentBeaconBlockRoot"].(string); ok { + expectedBeaconRoot := common.HexToHash(payloadBeaconRoot.(string)) + gotBeaconRoot := common.HexToHash(beaconRootStr) + if gotBeaconRoot != expectedBeaconRoot { + return fmt.Errorf("parentBeaconBlockRoot mismatch: got %s, want %s", gotBeaconRoot.Hex(), expectedBeaconRoot.Hex()) + } + } + // If parentBeaconBlockRoot was provided in payloadAttributes but missing in response, + // this may indicate the implementation is using an older ExecutionPayload version + } + } + + // Spec: "The client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the transactions array." + if txs, ok := executionPayload["transactions"].([]interface{}); ok { + if len(txs) != 0 { + return fmt.Errorf("expected 0 transactions (only from transactions array), got %d", len(txs)) + } + } else { + return fmt.Errorf("missing or invalid transactions in executionPayload") + } + + // Spec: "This method MUST NOT modify the client's canonical chain or head block." + // Verify chain head hasn't changed + newHead := t.chain.Head() + if newHead.Hash() != parentHash { + return fmt.Errorf("chain head changed (method should be read-only): was %s, now %s", parentHash.Hex(), newHead.Hash().Hex()) + } + + return nil + }, + }, + { + Name: "build-block-with-extra-data", + About: "builds a block with extraData but no transactions using testing_buildBlockV1", + Run: func(ctx context.Context, t *T) error { + // Get the parent block (head of chain) + parentBlock := t.chain.Head() + parentHash := parentBlock.Hash() + + // Create payload attributes + payloadAttrs := map[string]interface{}{ + "timestamp": hexutil.Uint64(parentBlock.Time() + 12), + "prevRandao": common.Hash{}.Hex(), + "suggestedFeeRecipient": common.Address{}.Hex(), + "withdrawals": []interface{}{}, + } + + // Add parentBeaconBlockRoot if Cancun is active + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + beaconRoot := common.Hash{0xcf, 0x8e, 0x0d, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x01, 0xd0, 0x79, 0x03, 0x47, 0x32, 0x03, 0x02, 0xcc, 0x09, 0x43, 0xd5, 0xa1, 0x88, 0x43, 0x65, 0x14, 0x9a, 0x42, 0x21, 0x2e, 0x88, 0x22} + payloadAttrs["parentBeaconBlockRoot"] = beaconRoot.Hex() + } + + // Prepare extraData (non-null) + extraData := hexutil.Encode([]byte("custom_extra_data")) + + // Call the RPC method with 4 separate parameters (extraData is non-null, transactions is empty) + var result map[string]interface{} + err := t.rpc.CallContext(ctx, &result, "testing_buildBlockV1", + parentHash.Hex(), // param 1: parentBlockHash + payloadAttrs, // param 2: payloadAttributes + []string{}, // param 3: transactions (empty array) + extraData, // param 4: extraData (non-null) + ) + if err != nil { + return fmt.Errorf("testing_buildBlockV1 call failed: %w", err) + } + + // Validate response structure + executionPayload, ok := result["executionPayload"].(map[string]interface{}) + if !ok { + return fmt.Errorf("missing or invalid executionPayload in response") + } + + // Verify parent hash matches + if parentHashStr, ok := executionPayload["parentHash"].(string); ok { + if parentHashStr != parentHash.Hex() { + return fmt.Errorf("parentHash mismatch: got %s, want %s", parentHashStr, parentHash.Hex()) + } + } else { + return fmt.Errorf("missing parentHash in executionPayload") + } + + // Verify block number is parent + 1 + if blockNumStr, ok := executionPayload["blockNumber"].(string); ok { + blockNum := hexutil.MustDecodeUint64(blockNumStr) + expectedNum := parentBlock.NumberU64() + 1 + if blockNum != expectedNum { + return fmt.Errorf("blockNumber mismatch: got %d, want %d", blockNum, expectedNum) + } + } else { + return fmt.Errorf("missing blockNumber in executionPayload") + } + + // Spec: "The client MUST use the provided payloadAttributes" + // Verify timestamp matches payloadAttributes + if timestampStr, ok := executionPayload["timestamp"].(string); ok { + gotTimestamp := hexutil.MustDecodeUint64(timestampStr) + expectedTimestamp := uint64(payloadAttrs["timestamp"].(hexutil.Uint64)) + if gotTimestamp != expectedTimestamp { + return fmt.Errorf("timestamp mismatch: got %d, want %d", gotTimestamp, expectedTimestamp) + } + } else { + return fmt.Errorf("missing timestamp in executionPayload") + } + + // Verify prevRandao matches payloadAttributes + if prevRandaoStr, ok := executionPayload["prevRandao"].(string); ok { + expectedPrevRandao := common.HexToHash(payloadAttrs["prevRandao"].(string)) + gotPrevRandao := common.HexToHash(prevRandaoStr) + if gotPrevRandao != expectedPrevRandao { + return fmt.Errorf("prevRandao mismatch: got %s, want %s", gotPrevRandao.Hex(), expectedPrevRandao.Hex()) + } + } else { + return fmt.Errorf("missing prevRandao in executionPayload") + } + + // Verify feeRecipient matches suggestedFeeRecipient from payloadAttributes + if feeRecipientStr, ok := executionPayload["feeRecipient"].(string); ok { + expectedFeeRecipient := common.HexToAddress(payloadAttrs["suggestedFeeRecipient"].(string)) + gotFeeRecipient := common.HexToAddress(feeRecipientStr) + if gotFeeRecipient != expectedFeeRecipient { + return fmt.Errorf("feeRecipient mismatch: got %s, want %s", gotFeeRecipient.Hex(), expectedFeeRecipient.Hex()) + } + } else { + return fmt.Errorf("missing feeRecipient in executionPayload") + } + + // Verify parentBeaconBlockRoot if Cancun is active and it's present in response + // Note: Some implementations may not include this field depending on ExecutionPayload version + if t.chain.Config().IsCancun(parentBlock.Number(), parentBlock.Time()) { + if payloadBeaconRoot, hasBeaconRoot := payloadAttrs["parentBeaconBlockRoot"]; hasBeaconRoot { + if beaconRootStr, ok := executionPayload["parentBeaconBlockRoot"].(string); ok { + expectedBeaconRoot := common.HexToHash(payloadBeaconRoot.(string)) + gotBeaconRoot := common.HexToHash(beaconRootStr) + if gotBeaconRoot != expectedBeaconRoot { + return fmt.Errorf("parentBeaconBlockRoot mismatch: got %s, want %s", gotBeaconRoot.Hex(), expectedBeaconRoot.Hex()) + } + } + // If parentBeaconBlockRoot was provided in payloadAttributes but missing in response, + // this may indicate the implementation is using an older ExecutionPayload version + } + } + + // Spec: "The client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the transactions array." + if txs, ok := executionPayload["transactions"].([]interface{}); ok { + if len(txs) != 0 { + return fmt.Errorf("expected 0 transactions (only from transactions array), got %d", len(txs)) + } + } else { + return fmt.Errorf("missing or invalid transactions in executionPayload") + } + + // Spec: "If extraData is provided, the client MUST set the extraData field of the resulting payload to this value." + if extraDataStr, ok := executionPayload["extraData"].(string); ok { + if extraDataStr != extraData { + return fmt.Errorf("extraData mismatch: got %s, want %s (per spec, must match provided value)", extraDataStr, extraData) + } + } else { + return fmt.Errorf("missing extraData in executionPayload") + } + + // Spec: "This method MUST NOT modify the client's canonical chain or head block." + // Verify chain head hasn't changed + newHead := t.chain.Head() + if newHead.Hash() != parentHash { + return fmt.Errorf("chain head changed (method should be read-only): was %s, now %s", parentHash.Hex(), newHead.Hash().Hex()) + } + + return nil + }, + }, + // NOTE: This test is currently commented out because the ability to call arbitrary block heights + // (i.e., using a parentHash that is not the current head) is not yet defined in the spec. + // The current implementation requires parentHash to match the current head, which prevents + // reconstructing past blocks. This test is left here in case the spec is updated to allow + // building blocks on top of arbitrary parent hashes. + /* + { + Name: "reconstruct-existing-block", + About: "reconstructs an existing block using testing_buildBlockV1 and verifies it matches exactly", + Run: func(ctx context.Context, t *T) error { + // Get the head block number + headBlock := t.chain.Head() + headNumber := headBlock.NumberU64() + + // Need at least block 1 to reconstruct (need a parent) + if headNumber < 1 { + return fmt.Errorf("chain too short: need at least block 1, got head at %d", headNumber) + } + + // Reconstruct the head block (or a recent block) + // Use the head block as the target to reconstruct + targetBlock := headBlock + targetNumber := targetBlock.NumberU64() + + // Get the parent block + parentBlock := t.chain.GetBlock(int(targetNumber - 1)) + if parentBlock == nil { + return fmt.Errorf("parent block %d not found", targetNumber-1) + } + + parentHash := parentBlock.Hash() + + // Extract payload attributes from the target block header + header := targetBlock.Header() + payloadAttrs := map[string]interface{}{ + "timestamp": hexutil.Uint64(header.Time), + "prevRandao": header.MixDigest.Hex(), + "suggestedFeeRecipient": header.Coinbase.Hex(), + "withdrawals": []interface{}{}, + } + + // Add parentBeaconBlockRoot if Cancun is active + // Note: The beacon root might not be directly accessible from the header in this version + // We'll use the same approach as other tests - set it if Cancun is active + if t.chain.Config().IsCancun(targetBlock.Number(), targetBlock.Time()) { + // For now, use a placeholder - the actual beacon root from the block + // might not be accessible, but we can still test the reconstruction + beaconRoot := common.Hash{0xcf, 0x8e, 0x0d, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x01, 0xd0, 0x79, 0x03, 0x47, 0x32, 0x03, 0x02, 0xcc, 0x09, 0x43, 0xd5, 0xa1, 0x88, 0x43, 0x65, 0x14, 0x9a, 0x42, 0x21, 0x2e, 0x88, 0x22} + payloadAttrs["parentBeaconBlockRoot"] = beaconRoot.Hex() + } + + // Extract transactions as RLP-encoded hex strings + // Per spec: "an array of raw, signed transactions (hex-encoded 0x... strings)" + transactions := make([]string, 0, len(targetBlock.Transactions())) + for _, tx := range targetBlock.Transactions() { + txBytes, err := tx.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal transaction: %w", err) + } + transactions = append(transactions, hexutil.Encode(txBytes)) + } + + // Extract extraData + var extraData interface{} + if len(header.Extra) > 0 { + extraData = hexutil.Encode(header.Extra) + } else { + extraData = nil + } + + // Call testing_buildBlockV1 to reconstruct the block + var result map[string]interface{} + err := t.rpc.CallContext(ctx, &result, "testing_buildBlockV1", + parentHash.Hex(), // param 1: parentBlockHash + payloadAttrs, // param 2: payloadAttributes + transactions, // param 3: transactions + extraData, // param 4: extraData + ) + if err != nil { + return fmt.Errorf("testing_buildBlockV1 call failed: %w", err) + } + + // Validate response structure + executionPayload, ok := result["executionPayload"].(map[string]interface{}) + if !ok { + return fmt.Errorf("missing or invalid executionPayload in response") + } + + // Compare block hash - should match exactly + if blockHashStr, ok := executionPayload["blockHash"].(string); ok { + gotHash := common.HexToHash(blockHashStr) + expectedHash := targetBlock.Hash() + if gotHash != expectedHash { + return fmt.Errorf("blockHash mismatch: got %s, want %s (blocks should be identical)", gotHash.Hex(), expectedHash.Hex()) + } + } else { + return fmt.Errorf("missing blockHash in executionPayload") + } + + // Compare parent hash + if parentHashStr, ok := executionPayload["parentHash"].(string); ok { + gotParentHash := common.HexToHash(parentHashStr) + if gotParentHash != parentHash { + return fmt.Errorf("parentHash mismatch: got %s, want %s", gotParentHash.Hex(), parentHash.Hex()) + } + } else { + return fmt.Errorf("missing parentHash in executionPayload") + } + + // Compare block number + if blockNumStr, ok := executionPayload["blockNumber"].(string); ok { + gotBlockNum := hexutil.MustDecodeUint64(blockNumStr) + if gotBlockNum != targetNumber { + return fmt.Errorf("blockNumber mismatch: got %d, want %d", gotBlockNum, targetNumber) + } + } else { + return fmt.Errorf("missing blockNumber in executionPayload") + } + + // Compare timestamp + if timestampStr, ok := executionPayload["timestamp"].(string); ok { + gotTimestamp := hexutil.MustDecodeUint64(timestampStr) + if gotTimestamp != header.Time { + return fmt.Errorf("timestamp mismatch: got %d, want %d", gotTimestamp, header.Time) + } + } else { + return fmt.Errorf("missing timestamp in executionPayload") + } + + // Compare state root + if stateRootStr, ok := executionPayload["stateRoot"].(string); ok { + gotStateRoot := common.HexToHash(stateRootStr) + if gotStateRoot != header.Root { + return fmt.Errorf("stateRoot mismatch: got %s, want %s", gotStateRoot.Hex(), header.Root.Hex()) + } + } else { + return fmt.Errorf("missing stateRoot in executionPayload") + } + + // Compare receipts root + if receiptsRootStr, ok := executionPayload["receiptsRoot"].(string); ok { + gotReceiptsRoot := common.HexToHash(receiptsRootStr) + if gotReceiptsRoot != header.ReceiptHash { + return fmt.Errorf("receiptsRoot mismatch: got %s, want %s", gotReceiptsRoot.Hex(), header.ReceiptHash.Hex()) + } + } else { + return fmt.Errorf("missing receiptsRoot in executionPayload") + } + + // Compare transactions - should match exactly in order + if txs, ok := executionPayload["transactions"].([]interface{}); ok { + if len(txs) != len(transactions) { + return fmt.Errorf("transaction count mismatch: got %d, want %d", len(txs), len(transactions)) + } + for i, txInterface := range txs { + if txStr, ok := txInterface.(string); ok { + if txStr != transactions[i] { + return fmt.Errorf("transaction %d mismatch: got %s, want %s", i, txStr, transactions[i]) + } + } else { + return fmt.Errorf("transaction %d is not a string", i) + } + } + } else { + return fmt.Errorf("missing or invalid transactions in executionPayload") + } + + // Compare extraData + if extraData != nil { + expectedExtraData := hexutil.Encode(header.Extra) + if extraDataStr, ok := executionPayload["extraData"].(string); ok { + if extraDataStr != expectedExtraData { + return fmt.Errorf("extraData mismatch: got %s, want %s", extraDataStr, expectedExtraData) + } + } else { + return fmt.Errorf("missing extraData in executionPayload") + } + } else { + // If extraData was nil, check if response has empty or default extraData + if extraDataStr, ok := executionPayload["extraData"].(string); ok { + // Some implementations may use default extraData when nil is provided + // This is acceptable as long as the block hash matches + _ = extraDataStr + } + } + + // Compare gas used + if gasUsedStr, ok := executionPayload["gasUsed"].(string); ok { + gotGasUsed := hexutil.MustDecodeUint64(gasUsedStr) + if gotGasUsed != header.GasUsed { + return fmt.Errorf("gasUsed mismatch: got %d, want %d", gotGasUsed, header.GasUsed) + } + } else { + return fmt.Errorf("missing gasUsed in executionPayload") + } + + // Compare gas limit + if gasLimitStr, ok := executionPayload["gasLimit"].(string); ok { + gotGasLimit := hexutil.MustDecodeUint64(gasLimitStr) + if gotGasLimit != header.GasLimit { + return fmt.Errorf("gasLimit mismatch: got %d, want %d", gotGasLimit, header.GasLimit) + } + } else { + return fmt.Errorf("missing gasLimit in executionPayload") + } + + // Compare base fee (if present) + if header.BaseFee != nil { + if baseFeeStr, ok := executionPayload["baseFeePerGas"].(string); ok { + gotBaseFee := new(big.Int).SetBytes(common.FromHex(baseFeeStr)) + if gotBaseFee.Cmp(header.BaseFee) != 0 { + return fmt.Errorf("baseFeePerGas mismatch: got %s, want %s", gotBaseFee.String(), header.BaseFee.String()) + } + } else { + return fmt.Errorf("missing baseFeePerGas in executionPayload (block has base fee)") + } + } + + // Verify chain head hasn't changed (read-only method) + newHead := t.chain.Head() + if newHead.Hash() != headBlock.Hash() { + return fmt.Errorf("chain head changed (method should be read-only): was %s, now %s", headBlock.Hash().Hex(), newHead.Hash().Hex()) + } + + return nil + }, + }, + */ + }, +} + var EthSimulateV1 = MethodTests{ "eth_simulateV1", []Test{