test: to avoid lag between tx inclusion and accessible state over RPC

for more info, https://github.com/ethereum/go-ethereum/issues/31798
This commit is contained in:
mmsqe 2025-05-18 13:42:09 +08:00
parent c8be0f9a74
commit 2f088053c9
No known key found for this signature in database
GPG key ID: 58B026FDE1D47518

View file

@ -0,0 +1,316 @@
package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/holiman/uint256"
"golang.org/x/sync/errgroup"
)
const (
GREEN_DOT = "\033[32m.\033[0m" // success
RED_X = "\033[31mx\033[0m" // expected failure case
YELLOW_S = "\033[33ms\033[0m" // unexpected failure, skip
basePort = 30000
numWorkers = 10
numTasks = 20
)
type UnexpectedCodeError struct {
Message string
}
func (e *UnexpectedCodeError) Error() string {
return e.Message
}
func waitForClient(port int, timeout time.Duration) (*ethclient.Client, error) {
endpoint := fmt.Sprintf("ws://127.0.0.1:%d", port)
start := time.Now()
for time.Since(start) < timeout {
client, err := ethclient.Dial(endpoint)
if err == nil {
return client, nil
}
time.Sleep(100 * time.Millisecond)
}
return nil, fmt.Errorf("failed to connect to client at %s within %s", endpoint, timeout)
}
func TestLagBetweenTxInclusionAndAccessibleState(t *testing.T) {
t.Parallel()
tasks := make(chan int, numTasks)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errG, _ := errgroup.WithContext(ctx)
for w := 0; w < numWorkers; w++ {
workerID := w
errG.Go(func() error {
for taskID := range tasks {
chErr := make(chan error, 1)
task := &task{
workerID: workerID,
taskID: taskID,
}
go func() {
chErr <- task.Run(t)
}()
select {
case <-ctx.Done():
t.Logf("Worker %d cancel task %d", workerID, taskID)
task.Cleanup(t)
return nil
case err := <-chErr:
if err != nil {
switch err.(type) {
case *UnexpectedCodeError:
fmt.Print(RED_X)
cancel()
t.Errorf("Worker %d failed on task %d with: %s", workerID, taskID, err.Error())
default:
fmt.Print(YELLOW_S, err)
}
} else {
fmt.Print(GREEN_DOT)
}
return err
}
}
return nil
})
}
for i := range numTasks {
tasks <- i
}
close(tasks)
errG.Wait()
}
type task struct {
workerID int
taskID int
testGethRef atomic.Pointer[testgeth]
}
func (task *task) Cleanup(t *testing.T) {
if g := task.testGethRef.Load(); g != nil && task.testGethRef.CompareAndSwap(g, nil) {
t.Logf("Worker %d cleanup", task.workerID)
g.Kill()
g.WaitExit()
}
}
func (task *task) Run(t *testing.T) (returnErr error) {
tmpDir, err := os.MkdirTemp("", fmt.Sprintf("geth-node-%d-%d-", task.workerID, task.taskID))
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
password := "123456"
pwdFilePath := filepath.Join(tmpDir, "password.txt")
pwdFile, err := os.Create(pwdFilePath)
if err != nil {
return err
}
defer os.Remove(pwdFilePath)
if _, err := pwdFile.WriteString(password + "\n"); err != nil {
return err
}
pwdFile.Close()
datadir := filepath.Join(tmpDir, "datadir")
ks := keystore.NewKeyStore(filepath.Join(datadir, "keystore"), keystore.LightScryptN, keystore.LightScryptP)
keyfileAcct, err := ks.NewAccount(password)
if err != nil {
return err
}
keyJson, err := os.ReadFile(keyfileAcct.URL.Path)
if err != nil {
return err
}
key, err := keystore.DecryptKey(keyJson, password)
if err != nil {
return err
}
port := basePort + task.workerID*100 + task.taskID
args := []string{
"--datadir",
datadir, // passing in a `datadir` is required to reproduce
"--dev",
"--dev.period",
"3",
"--ws",
"--ws.port",
fmt.Sprintf("%d", port),
"--ws.api",
"admin,eth,web3,debug",
"--unlock", keyfileAcct.Address.Hex(),
"--password", pwdFilePath,
}
g := runGeth(t, args...)
if !task.testGethRef.CompareAndSwap(nil, g) {
// task cancelled
go g.Kill()
return nil
}
defer func() {
task.Cleanup(t)
}()
client, err := waitForClient(port, 20*time.Second)
if err != nil {
return err
}
defer client.Close()
// fund new acct
ctx := context.Background()
value := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(22), nil)
gasLimit := uint64(21000)
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
return err
}
privateKey, err := crypto.GenerateKey()
if err != nil {
return err
}
newAcct := crypto.PubkeyToAddress(privateKey.PublicKey)
nonce, err := client.PendingNonceAt(ctx, keyfileAcct.Address)
if err != nil {
return err
}
tx := types.NewTransaction(nonce, newAcct, value, gasLimit, gasPrice, nil)
chainID, err := client.ChainID(ctx)
if err != nil {
return err
}
if err := sendSignedTransaction(ctx, client, tx, key.PrivateKey, chainID); err != nil {
return err
}
balance, err := client.BalanceAt(ctx, newAcct, nil)
if err != nil {
return err
}
if balance.Cmp(big.NewInt(0)) == 0 {
return errors.New("no funding yet")
}
// send 7702 tx
nonce, err = client.PendingNonceAt(ctx, newAcct)
if err != nil {
return err
}
signed, err := types.SignSetCode(privateKey, types.SetCodeAuthorization{
Address: common.HexToAddress("0xdeadbeef00000000000000000000000000000000"),
ChainID: *uint256.NewInt(chainID.Uint64()),
Nonce: nonce + 1,
})
if err != nil {
return err
}
codeTx := &types.SetCodeTx{
ChainID: uint256.NewInt(chainID.Uint64()),
Nonce: nonce,
Gas: 200000,
To: newAcct,
Value: uint256.NewInt(0),
Data: []byte("0x"),
GasTipCap: uint256.NewInt(10e11),
GasFeeCap: uint256.NewInt(10e11),
AccessList: []types.AccessTuple{},
AuthList: []types.SetCodeAuthorization{signed},
}
tx = types.NewTx(codeTx)
if err := sendSignedTransaction(ctx, client, tx, privateKey, chainID); err != nil {
return err
}
// the default block id is "latest"
code, err := client.CodeAt(ctx, newAcct, nil)
if err != nil {
return err
}
if !strings.HasPrefix(common.Bytes2Hex(code), "ef0100deadbeef") {
return &UnexpectedCodeError{Message: "Code was not set!"}
}
codeTx.Nonce = nonce + 2
signed, err = types.SignSetCode(privateKey, types.SetCodeAuthorization{
Address: common.HexToAddress("0x0000000000000000000000000000000000000000"),
ChainID: *uint256.NewInt(chainID.Uint64()),
Nonce: nonce + 3,
})
if err != nil {
return err
}
codeTx.AuthList = []types.SetCodeAuthorization{signed}
// clear code
clearTx := types.NewTx(codeTx)
if err := sendSignedTransaction(ctx, client, clearTx, privateKey, chainID); err != nil {
return err
}
clearedCode, err := client.CodeAt(ctx, newAcct, nil)
if err != nil {
return err
}
if len(clearedCode) != 0 {
return &UnexpectedCodeError{Message: "Code was not cleared!"}
}
return nil
}
func sendSignedTransaction(
ctx context.Context,
client *ethclient.Client,
tx *types.Transaction,
privateKey *ecdsa.PrivateKey,
chainID *big.Int,
) error {
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey)
if err != nil {
return err
}
if err := client.SendTransaction(ctx, signedTx); err != nil {
return err
}
for {
_, err := client.TransactionReceipt(ctx, signedTx.Hash())
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
return nil
}