Skip to content

Commit 47187df

Browse files
Simplify evm execution
1 parent e476a9c commit 47187df

10 files changed

Lines changed: 928 additions & 448 deletions

File tree

‎apps/evm/based/go.mod‎

Lines changed: 156 additions & 1 deletion
Large diffs are not rendered by default.

‎apps/evm/based/go.sum‎

Lines changed: 176 additions & 7 deletions
Large diffs are not rendered by default.

‎apps/evm/single/go.mod‎

Lines changed: 156 additions & 1 deletion
Large diffs are not rendered by default.

‎apps/evm/single/go.sum‎

Lines changed: 176 additions & 7 deletions
Large diffs are not rendered by default.

‎execution/evm/evm_helpers.go‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//go:build evm
2+
// +build evm
3+
4+
package evm
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"crypto/rand"
10+
"encoding/hex"
11+
"encoding/json"
12+
"fmt"
13+
"math/big"
14+
"net/http"
15+
"os"
16+
"path/filepath"
17+
"strings"
18+
"testing"
19+
"time"
20+
21+
"github.com/ethereum/go-ethereum/common"
22+
"github.com/ethereum/go-ethereum/core/types"
23+
"github.com/ethereum/go-ethereum/crypto"
24+
"github.com/ethereum/go-ethereum/ethclient"
25+
"github.com/golang-jwt/jwt/v5"
26+
"github.com/stretchr/testify/require"
27+
tc "github.com/testcontainers/testcontainers-go/modules/compose"
28+
)
29+
30+
// JWT Helpers
31+
func GenerateJWTSecret() (string, error) {
32+
jwtSecret := make([]byte, 32)
33+
_, err := rand.Read(jwtSecret)
34+
if err != nil {
35+
return "", fmt.Errorf("failed to generate random bytes: %w", err)
36+
}
37+
return hex.EncodeToString(jwtSecret), nil
38+
}
39+
40+
func DecodeSecret(jwtSecret string) ([]byte, error) {
41+
secret, err := hex.DecodeString(strings.TrimPrefix(jwtSecret, "0x"))
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to decode JWT secret: %w", err)
44+
}
45+
return secret, nil
46+
}
47+
48+
func GetAuthToken(jwtSecret []byte) (string, error) {
49+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
50+
"exp": time.Now().Add(time.Hour * 1).Unix(),
51+
"iat": time.Now().Unix(),
52+
})
53+
authToken, err := token.SignedString(jwtSecret)
54+
if err != nil {
55+
return "", fmt.Errorf("failed to sign JWT token: %w", err)
56+
}
57+
return authToken, nil
58+
}
59+
60+
// Docker Compose Setup for Reth Engine
61+
func SetupTestRethEngine(t *testing.T, dockerPath, jwtFilename string) string {
62+
dockerAbsPath, err := filepath.Abs(dockerPath)
63+
require.NoError(t, err)
64+
jwtPath := filepath.Join(dockerAbsPath, "jwttoken")
65+
err = os.MkdirAll(jwtPath, 0750)
66+
require.NoError(t, err)
67+
jwtSecret, err := GenerateJWTSecret()
68+
require.NoError(t, err)
69+
jwtFile := filepath.Join(jwtPath, jwtFilename)
70+
err = os.WriteFile(jwtFile, []byte(jwtSecret), 0600)
71+
require.NoError(t, err)
72+
t.Cleanup(func() { _ = os.Remove(jwtFile) })
73+
composeFilePath := filepath.Join(dockerAbsPath, "docker-compose.yml")
74+
identifier := tc.StackIdentifier(strings.ToLower(t.Name()))
75+
identifier = tc.StackIdentifier(strings.ReplaceAll(string(identifier), "/", "_"))
76+
identifier = tc.StackIdentifier(strings.ReplaceAll(string(identifier), " ", "_"))
77+
compose, err := tc.NewDockerComposeWith(tc.WithStackFiles(composeFilePath), identifier)
78+
require.NoError(t, err, "Failed to create docker compose")
79+
t.Cleanup(func() {
80+
ctx := context.Background()
81+
err := compose.Down(ctx, tc.RemoveOrphans(true), tc.RemoveVolumes(true))
82+
if err != nil {
83+
t.Logf("Warning: Failed to tear down docker-compose environment: %v", err)
84+
}
85+
})
86+
ctx := context.Background()
87+
err = compose.Up(ctx, tc.Wait(true))
88+
require.NoError(t, err, "Failed to start docker compose")
89+
err = WaitForRethContainer(t, jwtSecret, "http://localhost:8545", "http://localhost:8551")
90+
require.NoError(t, err)
91+
return jwtSecret
92+
}
93+
94+
// Wait for Reth container to be ready
95+
func WaitForRethContainer(t *testing.T, jwtSecret, ethURL, engineURL string) error {
96+
t.Helper()
97+
client := &http.Client{Timeout: 100 * time.Millisecond}
98+
timer := time.NewTimer(30 * time.Second)
99+
defer timer.Stop()
100+
for {
101+
select {
102+
case <-timer.C:
103+
return fmt.Errorf("timeout waiting for reth container to be ready")
104+
default:
105+
rpcReq := strings.NewReader(`{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}`)
106+
resp, err := client.Post(ethURL, "application/json", rpcReq)
107+
if err == nil {
108+
if err := resp.Body.Close(); err != nil {
109+
return fmt.Errorf("failed to close response body: %w", err)
110+
}
111+
if resp.StatusCode == http.StatusOK {
112+
req, err := http.NewRequest("POST", engineURL, strings.NewReader(`{"jsonrpc":"2.0","method":"engine_getClientVersionV1","params":[],"id":1}`))
113+
if err != nil {
114+
return err
115+
}
116+
req.Header.Set("Content-Type", "application/json")
117+
secret, err := DecodeSecret(jwtSecret)
118+
if err != nil {
119+
return err
120+
}
121+
authToken, err := GetAuthToken(secret)
122+
if err != nil {
123+
return err
124+
}
125+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken))
126+
resp, err := client.Do(req)
127+
if err == nil {
128+
if err := resp.Body.Close(); err != nil {
129+
return fmt.Errorf("failed to close response body: %w", err)
130+
}
131+
if resp.StatusCode == http.StatusOK {
132+
return nil
133+
}
134+
}
135+
}
136+
}
137+
time.Sleep(100 * time.Millisecond)
138+
}
139+
}
140+
}
141+
142+
// Transaction Helpers
143+
func GetRandomTransaction(t *testing.T, privateKeyHex, toAddressHex, chainID string, gasLimit uint64, lastNonce *uint64) *types.Transaction {
144+
privateKey, err := crypto.HexToECDSA(privateKeyHex)
145+
require.NoError(t, err)
146+
chainId, ok := new(big.Int).SetString(chainID, 10)
147+
require.True(t, ok)
148+
txValue := big.NewInt(1000000000000000000)
149+
gasPrice := big.NewInt(30000000000)
150+
toAddress := common.HexToAddress(toAddressHex)
151+
data := make([]byte, 16)
152+
_, err = rand.Read(data)
153+
require.NoError(t, err)
154+
tx := types.NewTx(&types.LegacyTx{
155+
Nonce: *lastNonce,
156+
To: &toAddress,
157+
Value: txValue,
158+
Gas: gasLimit,
159+
GasPrice: gasPrice,
160+
Data: data,
161+
})
162+
*lastNonce++
163+
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainId), privateKey)
164+
require.NoError(t, err)
165+
return signedTx
166+
}
167+
168+
func SubmitTransaction(t *testing.T, tx *types.Transaction) {
169+
rpcClient, err := ethclient.Dial("http://localhost:8545")
170+
require.NoError(t, err)
171+
defer rpcClient.Close()
172+
173+
err = rpcClient.SendTransaction(context.Background(), tx)
174+
require.NoError(t, err)
175+
}
176+
177+
func CheckTxIncluded(t *testing.T, txHash common.Hash) bool {
178+
rpcClient, err := ethclient.Dial("http://localhost:8545")
179+
if err != nil {
180+
return false
181+
}
182+
defer rpcClient.Close()
183+
receipt, err := rpcClient.TransactionReceipt(context.Background(), txHash)
184+
return err == nil && receipt != nil && receipt.Status == 1
185+
}
186+
187+
// Genesis Hash Helper
188+
func GetGenesisHash(t *testing.T) string {
189+
client := &http.Client{Timeout: 2 * time.Second}
190+
data := []byte(`{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x0", false],"id":1}`)
191+
resp, err := client.Post("http://localhost:8545", "application/json", bytes.NewReader(data))
192+
require.NoError(t, err)
193+
defer resp.Body.Close()
194+
var result struct {
195+
Result struct {
196+
Hash string `json:"hash"`
197+
} `json:"result"`
198+
}
199+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
200+
require.NotEmpty(t, result.Result.Hash)
201+
return result.Result.Hash
202+
}

‎execution/evm/execution.go‎

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ type EngineClient struct {
4242
initialHeight uint64
4343
feeRecipient common.Address // Address to receive transaction fees
4444

45-
lastHeadBlockHash common.Hash // Store last non-finalized HeadBlockHash
46-
lastSafeBlockHash common.Hash // Store last non-finalized SafeBlockHash
47-
lastFinalizedBlockHash common.Hash // Store last finalized block hash
48-
mu sync.Mutex // Mutex to protect concurrent access to mutable state
45+
currentHeadBlockHash common.Hash // Store last non-finalized HeadBlockHash
46+
currentSafeBlockHash common.Hash // Store last non-finalized SafeBlockHash
47+
currentFinalizedBlockHash common.Hash // Store last finalized block hash
48+
mu sync.Mutex // Mutex to protect concurrent access to mutable state
4949
}
5050

5151
// NewEngineExecutionClient creates a new instance of EngineAPIExecutionClient
@@ -278,24 +278,19 @@ func (c *EngineClient) ExecuteTxs(ctx context.Context, txs [][]byte, blockHeight
278278
func (c *EngineClient) setFinal(ctx context.Context, blockHash common.Hash, isFinal bool) error {
279279
var args engine.ForkchoiceStateV1
280280

281+
// Update block hashes based on finalization status
281282
if isFinal {
282-
// Use the last stored values for head and safe block hashes
283-
args = engine.ForkchoiceStateV1{
284-
HeadBlockHash: c.lastHeadBlockHash,
285-
SafeBlockHash: c.lastSafeBlockHash,
286-
FinalizedBlockHash: blockHash,
287-
}
288-
// Store the current blockHash as the latest finalized block hash
289-
c.lastFinalizedBlockHash = blockHash
283+
c.currentFinalizedBlockHash = blockHash
290284
} else {
291-
// Store the current blockHash as the latest head and safe block hashes
292-
c.lastHeadBlockHash = blockHash
293-
c.lastSafeBlockHash = blockHash
294-
args = engine.ForkchoiceStateV1{
295-
HeadBlockHash: blockHash,
296-
SafeBlockHash: blockHash,
297-
FinalizedBlockHash: c.lastFinalizedBlockHash,
298-
}
285+
c.currentHeadBlockHash = blockHash
286+
c.currentSafeBlockHash = blockHash
287+
}
288+
289+
// Construct forkchoice state
290+
args = engine.ForkchoiceStateV1{
291+
HeadBlockHash: c.currentHeadBlockHash,
292+
SafeBlockHash: c.currentSafeBlockHash,
293+
FinalizedBlockHash: c.currentFinalizedBlockHash,
299294
}
300295

301296
var forkchoiceResult engine.ForkChoiceResponse

0 commit comments

Comments
 (0)