mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 06:36:43 +00:00
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:
parent
c8be0f9a74
commit
2f088053c9
1 changed files with 316 additions and 0 deletions
316
cmd/geth/lag_between_tx_inclusion_test.go
Normal file
316
cmd/geth/lag_between_tx_inclusion_test.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue