diff --git a/cmd/geth/lag_between_tx_inclusion_test.go b/cmd/geth/lag_between_tx_inclusion_test.go new file mode 100644 index 0000000000..1009d1b9ec --- /dev/null +++ b/cmd/geth/lag_between_tx_inclusion_test.go @@ -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 +}