From 206e3d0dacbfefd883f2ae7483e24aa7c33b9c80 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 19 May 2026 15:58:47 +0800 Subject: [PATCH 1/6] Add EIP2935 e2e test --- tests/eip2935/eip2935_test.go | 239 ++++++++++++++++++++++++++++++++++ tests/eip2935/main_test.go | 15 +++ 2 files changed, 254 insertions(+) create mode 100644 tests/eip2935/eip2935_test.go create mode 100644 tests/eip2935/main_test.go diff --git a/tests/eip2935/eip2935_test.go b/tests/eip2935/eip2935_test.go new file mode 100644 index 0000000..0549d44 --- /dev/null +++ b/tests/eip2935/eip2935_test.go @@ -0,0 +1,239 @@ +package eip2935 + +// EIP-2935 adds a historical block-hash facade contract activated at the +// INTERSTELLAR fork. +// +// Contract address: 0x0000F90827F1C53a10cb7A02335B175320002935 (fixed by EIP) +// Calldata: exactly 32 bytes — a uint256 block number, NO function selector. +// Dispatch goes through the fallback function. +// +// Return semantics: +// - valid input → 32-byte block ID for that block number +// - any failure → revert with empty returndata (matches EIP-2935 reference) +// +// Validity rules (post-fork): +// - input length must be exactly 32 bytes +// - num < block.number (no future / current block) +// - block.number - num <= 8191 (within SERVE_WINDOW) +// +// Pre-fork: the address has no code, so calls to it succeed with empty +// output and do not revert (standard EVM behavior for empty accounts). + +import ( + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vechain/thor/v2/api" + "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/thorclient" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +// historyAddr is the fixed EIP-2935 facade address. +var historyAddr = thor.MustParseAddress("0x0000F90827F1C53a10cb7A02335B175320002935") + +// encodeBlockNumber returns the 32-byte big-endian uint256 calldata expected +// by the History contract (no function selector — fallback dispatch). +func encodeBlockNumber(num uint32) []byte { + var data [32]byte + new(big.Int).SetUint64(uint64(num)).FillBytes(data[:]) + return data[:] +} + +// waitForBlockAtLeast polls until best.Number >= min, so tests have a +// historical block to query (target = best - 1 must be >= 0). +func waitForBlockAtLeast(t *testing.T, client *thorclient.Client, min uint32, timeout time.Duration) *api.JSONCollapsedBlock { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + b, err := client.Block("best") + require.NoError(t, err) + if b.Number >= min { + return b + } + time.Sleep(1 * time.Second) + } + t.Fatalf("timed out waiting for block number >= %d", min) + return nil +} + +// TestEIP2935_BlockID verifies that History.BlockID(num) returns the canonical +// block ID for an in-range historical block, mirroring the canonical lookup +// served by the blocks API. +func TestEIP2935_BlockID(t *testing.T) { + client := helper.NewClient(nodeURL) + + // Need at least 2 blocks so target = best - 1 is in the SERVE_WINDOW. + best := waitForBlockAtLeast(t, client, 2, 30*time.Second) + target := best.Number - 1 + + // Pin both the evaluation revision and the canonical lookup to best.Number + // to avoid races against the chain producing further blocks underneath us. + bestRev := strconv.FormatUint(uint64(best.Number), 10) + targetRev := strconv.FormatUint(uint64(target), 10) + + canonical, err := client.Block(targetRev) + require.NoError(t, err) + + callData := &api.BatchCallData{ + Clauses: api.Clauses{{To: &historyAddr, Data: "0x" + hex.EncodeToString(encodeBlockNumber(target))}}, + Gas: 50_000, + } + + t.Run("pre-fork", func(t *testing.T) { + + historyAcc, err := client.Account(&historyAddr, thorclient.Revision(helper.PreForkRevision)) + require.NoError(t, err) + require.NotNil(t, historyAcc) + require.False(t, historyAcc.HasCode, "pre-fork: History contract must have no code") + + results, err := client.InspectClauses(callData, thorclient.Revision(helper.PreForkRevision)) + require.NoError(t, err) + require.Len(t, results, 1) + assert.False(t, results[0].Reverted, "pre-fork: address has no code, call must not revert") + assert.Empty(t, strings.TrimPrefix(results[0].Data, "0x"), + "pre-fork: address has no code, output must be empty") + }) + + t.Run("post-fork", func(t *testing.T) { + + historyAcc, err := client.Account(&historyAddr, thorclient.Revision(bestRev)) + require.NoError(t, err) + require.NotNil(t, historyAcc) + require.True(t, historyAcc.HasCode, "post-fork: History contract must have code") + + results, err := client.InspectClauses(callData, thorclient.Revision(bestRev)) + require.NoError(t, err) + require.Len(t, results, 1) + require.False(t, results[0].Reverted, + "post-fork: BlockID(best-1) must not revert (vmError: %s)", results[0].VMError) + + raw, err := hex.DecodeString(strings.TrimPrefix(results[0].Data, "0x")) + require.NoError(t, err) + require.Len(t, raw, 32, "post-fork: History must return a 32-byte block ID") + assert.Equal(t, canonical.ID.Bytes(), raw, + "post-fork: History.BlockID(n) must equal canonical block n ID") + }) +} + +// TestEIP2935_FutureBlockReverts verifies that requesting a block number +// equal to or above the current best reverts post-fork with no return data. +// EIP-2935 SERVE_WINDOW is [best-8191, best-1]. +func TestEIP2935_FutureBlockReverts(t *testing.T) { + client := helper.NewClient(nodeURL) + + best, err := client.Block("best") + require.NoError(t, err) + bestRev := strconv.FormatUint(uint64(best.Number), 10) + + cases := []struct { + name string + num uint32 + }{ + {"equal-to-best", best.Number}, + {"far-future", best.Number + 9999}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + callData := &api.BatchCallData{ + Clauses: api.Clauses{{To: &historyAddr, Data: "0x" + hex.EncodeToString(encodeBlockNumber(tc.num))}}, + Gas: 50_000, + } + + t.Run("pre-fork", func(t *testing.T) { + results, err := client.InspectClauses(callData, thorclient.Revision(helper.PreForkRevision)) + require.NoError(t, err) + require.Len(t, results, 1) + assert.False(t, results[0].Reverted, "pre-fork: no code at address — must not revert") + assert.Empty(t, strings.TrimPrefix(results[0].Data, "0x"), + "pre-fork: output must be empty") + }) + + t.Run("post-fork", func(t *testing.T) { + results, err := client.InspectClauses(callData, thorclient.Revision(bestRev)) + require.NoError(t, err) + require.Len(t, results, 1) + assert.True(t, results[0].Reverted, "post-fork: out-of-range block must revert") + assert.Empty(t, strings.TrimPrefix(results[0].Data, "0x"), + "post-fork: EIP-2935 revert must carry no return data") + }) + }) + } +} + +// TestEIP2935_InvalidLength verifies that calldata not exactly 32 bytes long +// reverts with no return data post-fork (input.length != 32 → revert()). +func TestEIP2935_InvalidLength(t *testing.T) { + client := helper.NewClient(nodeURL) + + for _, length := range []int{0, 31, 33} { + t.Run(fmt.Sprintf("length-%d", length), func(t *testing.T) { + callData := &api.BatchCallData{ + Clauses: api.Clauses{{To: &historyAddr, Data: "0x" + hex.EncodeToString(make([]byte, length))}}, + Gas: 50_000, + } + + t.Run("pre-fork", func(t *testing.T) { + results, err := client.InspectClauses(callData, thorclient.Revision(helper.PreForkRevision)) + require.NoError(t, err) + require.Len(t, results, 1) + assert.False(t, results[0].Reverted, "pre-fork: no code at address — must not revert") + assert.Empty(t, strings.TrimPrefix(results[0].Data, "0x"), + "pre-fork: output must be empty") + }) + + t.Run("post-fork", func(t *testing.T) { + results, err := client.InspectClauses(callData, thorclient.Revision("best")) + require.NoError(t, err) + require.Len(t, results, 1) + assert.True(t, results[0].Reverted, "post-fork: length=%d must revert", length) + assert.Empty(t, strings.TrimPrefix(results[0].Data, "0x"), + "post-fork: EIP-2935 revert must carry no return data") + }) + }) + } +} + +// TestEIP2935_Revision verifies that pinning the call to an explicit revision +// matches the canonical block lookup, and that a malformed revision string +// surfaces as a request-level error rather than a silent empty result. +func TestEIP2935_Revision(t *testing.T) { + client := helper.NewClient(nodeURL) + + best := waitForBlockAtLeast(t, client, 2, 30*time.Second) + target := best.Number - 1 + bestRev := strconv.FormatUint(uint64(best.Number), 10) + + callData := &api.BatchCallData{ + Clauses: api.Clauses{{To: &historyAddr, Data: "0x" + hex.EncodeToString(encodeBlockNumber(target))}}, + Gas: 50_000, + } + + // Pin to best.Number explicitly — must match the canonical block ID. + results, err := client.InspectClauses(callData, thorclient.Revision(bestRev)) + require.NoError(t, err) + require.Len(t, results, 1) + require.False(t, results[0].Reverted, + "pinned revision call must not revert (vmError: %s)", results[0].VMError) + pinned, err := hex.DecodeString(strings.TrimPrefix(results[0].Data, "0x")) + require.NoError(t, err) + require.Len(t, pinned, 32) + + canonical, err := client.Block(strconv.FormatUint(uint64(target), 10)) + require.NoError(t, err) + assert.Equal(t, canonical.ID.Bytes(), pinned, + "pinned-revision History.BlockID(n) must match canonical block n ID") + + // Bad revision string should surface as a request error. + _, err = client.InspectClauses(callData, thorclient.Revision("not-a-real-revision")) + assert.Error(t, err, "malformed revision must surface as a request error") +} diff --git a/tests/eip2935/main_test.go b/tests/eip2935/main_test.go new file mode 100644 index 0000000..24fed41 --- /dev/null +++ b/tests/eip2935/main_test.go @@ -0,0 +1,15 @@ +package eip2935 + +import ( + "os" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Setenv("THOR_BRANCH", "718bd161a70ba3ab7deadff6bce639ea95e2b546") + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +} From df29a4f80a0244bd3a5c69166e42ad66015af625 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 19 May 2026 16:02:23 +0800 Subject: [PATCH 2/6] Update README.md --- .github/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/README.md b/.github/README.md index c35e568..0c7ede0 100644 --- a/.github/README.md +++ b/.github/README.md @@ -12,6 +12,7 @@ End-to-end tests for the VeChain **INTERSTELLAR** fork, which activates at block | `tests/eip7934` | [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934) | Max RLP-encoded block size (`MaxRLPBlockSize = 8_388_608`); packer-level split test + P2P consensus-level rejection of oversized blocks | | `tests/eip7883` | [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883) | ModExp precompile repricing | | `tests/eip7939` | [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) | `CLZ` opcode (0x1e) — count leading zeros | +| `tests/eip2935` | [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) | Serve historical block hashes from state | ## Repository layout @@ -30,6 +31,7 @@ interstellar-e2e/ ├── eip7934/ ├── eip7883/ └── eip7939/ + └── eip2935/ ``` ## Prerequisites From b79e72b5a9b533b8019faec0d89eb0983147402e Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Tue, 19 May 2026 16:03:39 +0800 Subject: [PATCH 3/6] Remove branch target --- tests/eip2935/main_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/eip2935/main_test.go b/tests/eip2935/main_test.go index 24fed41..48eccce 100644 --- a/tests/eip2935/main_test.go +++ b/tests/eip2935/main_test.go @@ -10,6 +10,5 @@ import ( var nodeURL string func TestMain(m *testing.M) { - os.Setenv("THOR_BRANCH", "718bd161a70ba3ab7deadff6bce639ea95e2b546") os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } From 59f80d4d350e2557011982f5304e01fec1448b5b Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Wed, 20 May 2026 18:44:23 +0800 Subject: [PATCH 4/6] add tag commit hash for testing --- network/go.mod | 2 +- network/go.sum | 4 ++-- tests/eip2935/main_test.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/network/go.mod b/network/go.mod index 9e24ce4..9f70e31 100644 --- a/network/go.mod +++ b/network/go.mod @@ -3,7 +3,7 @@ module github.com/vechain/interstellar-e2e/network go 1.26.1 require ( - github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2 + github.com/vechain/networkhub v0.0.8 github.com/vechain/thor/v2 v2.4.3 ) diff --git a/network/go.sum b/network/go.sum index 4998d03..1bbbb6b 100644 --- a/network/go.sum +++ b/network/go.sum @@ -172,8 +172,8 @@ github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e h1:0/g3bVEx github.com/vechain/go-ethereum v1.8.15-0.20260324060835-4fc778eca93e/go.mod h1:LVuf3xPnVtHmoIP5+mN7aPnIeRBgo0xVq/wVELtSeIA= github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655 h1:CbHcWpCi7wOYfpoErRABh3Slyq9vO0Ay/EHN5GuJSXQ= github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2 h1:dblLJGXsIvllG5P9kMsHiGnoNhy4X0DbXyzIneatous= -github.com/vechain/networkhub v0.0.8-0.20260331132751-a070cb8f5bd2/go.mod h1:lExTZ9CKxmH4pOK6z1TSa76aj8tlOieenHXxK2E8hpM= +github.com/vechain/networkhub v0.0.8 h1:HKdKvtEkNxomgVmofmrW+3IRuI+yf6Hfyeu0lLykbpw= +github.com/vechain/networkhub v0.0.8/go.mod h1:lExTZ9CKxmH4pOK6z1TSa76aj8tlOieenHXxK2E8hpM= github.com/vechain/thor/v2 v2.4.3 h1:bz0WjvnbhOe6x99ngA8hdwF7v8WroR+cZqa9k4F+mBc= github.com/vechain/thor/v2 v2.4.3/go.mod h1:AjHV4eiral0P8LdS/dI16Og2APp/tT+IAO2fMok+ufQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/tests/eip2935/main_test.go b/tests/eip2935/main_test.go index 48eccce..24fed41 100644 --- a/tests/eip2935/main_test.go +++ b/tests/eip2935/main_test.go @@ -10,5 +10,6 @@ import ( var nodeURL string func TestMain(m *testing.M) { + os.Setenv("THOR_BRANCH", "718bd161a70ba3ab7deadff6bce639ea95e2b546") os.Exit(helper.RunTestMain(m, &nodeURL, nil)) } From a7d086156e499d4d9a4a93e8ee33ad245d22a224 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Wed, 20 May 2026 18:53:31 +0800 Subject: [PATCH 5/6] fix lint --- tests/eip2935/eip2935_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/eip2935/eip2935_test.go b/tests/eip2935/eip2935_test.go index 0549d44..5f1ceaa 100644 --- a/tests/eip2935/eip2935_test.go +++ b/tests/eip2935/eip2935_test.go @@ -89,7 +89,6 @@ func TestEIP2935_BlockID(t *testing.T) { } t.Run("pre-fork", func(t *testing.T) { - historyAcc, err := client.Account(&historyAddr, thorclient.Revision(helper.PreForkRevision)) require.NoError(t, err) require.NotNil(t, historyAcc) @@ -104,7 +103,6 @@ func TestEIP2935_BlockID(t *testing.T) { }) t.Run("post-fork", func(t *testing.T) { - historyAcc, err := client.Account(&historyAddr, thorclient.Revision(bestRev)) require.NoError(t, err) require.NotNil(t, historyAcc) From 8618cd78872649b22e979b16effbc67ad673b363 Mon Sep 17 00:00:00 2001 From: moglu2017 Date: Thu, 21 May 2026 16:53:43 +0800 Subject: [PATCH 6/6] Removes hardcoded environment variable from test setup. --- tests/eip2935/main_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/eip2935/main_test.go b/tests/eip2935/main_test.go index 24fed41..48eccce 100644 --- a/tests/eip2935/main_test.go +++ b/tests/eip2935/main_test.go @@ -10,6 +10,5 @@ import ( var nodeURL string func TestMain(m *testing.M) { - os.Setenv("THOR_BRANCH", "718bd161a70ba3ab7deadff6bce639ea95e2b546") os.Exit(helper.RunTestMain(m, &nodeURL, nil)) }