github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/docker/test_env/geth.go (about)

     1  package test_env
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/docker/go-connections/nat"
    17  	"github.com/ethereum/go-ethereum/accounts"
    18  	"github.com/ethereum/go-ethereum/accounts/keystore"
    19  	"github.com/ethereum/go-ethereum/rpc"
    20  	"github.com/google/uuid"
    21  	"github.com/rs/zerolog"
    22  	"github.com/rs/zerolog/log"
    23  	tc "github.com/testcontainers/testcontainers-go"
    24  	tcwait "github.com/testcontainers/testcontainers-go/wait"
    25  
    26  	"github.com/smartcontractkit/chainlink-testing-framework/libs/blockchain"
    27  	"github.com/smartcontractkit/chainlink-testing-framework/libs/docker"
    28  	"github.com/smartcontractkit/chainlink-testing-framework/libs/logging"
    29  	"github.com/smartcontractkit/chainlink-testing-framework/libs/mirror"
    30  	"github.com/smartcontractkit/chainlink-testing-framework/libs/utils/templates"
    31  	"github.com/smartcontractkit/chainlink-testing-framework/libs/utils/testcontext"
    32  )
    33  
    34  const (
    35  	// RootFundingAddr is the static key that hardhat is using
    36  	// https://hardhat.org/hardhat-runner/docs/getting-started
    37  	// if you need more keys, keep them compatible, so we can swap Geth to Ganache/Hardhat in the future
    38  	RootFundingAddr   = `0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266`
    39  	RootFundingWallet = `{"address":"f39fd6e51aad88f6f4ce6ab8827279cfffb92266","crypto":{"cipher":"aes-128-ctr","ciphertext":"c36afd6e60b82d6844530bd6ab44dbc3b85a53e826c3a7f6fc6a75ce38c1e4c6","cipherparams":{"iv":"f69d2bb8cd0cb6274535656553b61806"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"80d5f5e38ba175b6b89acfc8ea62a6f163970504af301292377ff7baafedab53"},"mac":"f2ecec2c4d05aacc10eba5235354c2fcc3776824f81ec6de98022f704efbf065"},"id":"e5c124e9-e280-4b10-a27b-d7f3e516b408","version":3}`
    40  
    41  	TX_GETH_HTTP_PORT = "8544"
    42  	TX_GETH_WS_PORT   = "8545"
    43  )
    44  
    45  type InternalDockerUrls struct {
    46  	HttpUrl string
    47  	WsUrl   string
    48  }
    49  
    50  type Geth struct {
    51  	EnvComponent
    52  	ExternalHttpUrl string
    53  	InternalHttpUrl string
    54  	ExternalWsUrl   string
    55  	InternalWsUrl   string
    56  	chainConfig     *EthereumChainConfig
    57  	l               zerolog.Logger
    58  	t               *testing.T
    59  }
    60  
    61  func NewGeth(networks []string, chainConfig *EthereumChainConfig, opts ...EnvComponentOption) *Geth {
    62  	dockerImage, err := mirror.GetImage("ethereum/client-go:v1.12")
    63  	if err != nil {
    64  		return nil
    65  	}
    66  
    67  	parts := strings.Split(dockerImage, ":")
    68  	g := &Geth{
    69  		EnvComponent: EnvComponent{
    70  			ContainerName:    fmt.Sprintf("%s-%s", "geth", uuid.NewString()[0:8]),
    71  			Networks:         networks,
    72  			ContainerImage:   parts[0],
    73  			ContainerVersion: parts[1],
    74  		},
    75  		chainConfig: chainConfig,
    76  		l:           log.Logger,
    77  	}
    78  	g.SetDefaultHooks()
    79  	for _, opt := range opts {
    80  		opt(&g.EnvComponent)
    81  	}
    82  	return g
    83  }
    84  
    85  func (g *Geth) WithLogger(l zerolog.Logger) *Geth {
    86  	g.l = l
    87  	return g
    88  }
    89  
    90  func (g *Geth) WithTestInstance(t *testing.T) *Geth {
    91  	g.l = logging.GetTestLogger(t)
    92  	g.t = t
    93  	return g
    94  }
    95  
    96  func (g *Geth) StartContainer() (blockchain.EVMNetwork, InternalDockerUrls, error) {
    97  	r, _, _, err := g.getGethContainerRequest(g.Networks)
    98  	if err != nil {
    99  		return blockchain.EVMNetwork{}, InternalDockerUrls{}, err
   100  	}
   101  
   102  	l := logging.GetTestContainersGoTestLogger(g.t)
   103  	ct, err := docker.StartContainerWithRetry(g.l, tc.GenericContainerRequest{
   104  		ContainerRequest: *r,
   105  		Reuse:            true,
   106  		Started:          true,
   107  		Logger:           l,
   108  	})
   109  	if err != nil {
   110  		return blockchain.EVMNetwork{}, InternalDockerUrls{}, fmt.Errorf("cannot start geth container: %w", err)
   111  	}
   112  	host, err := GetHost(testcontext.Get(g.t), ct)
   113  	if err != nil {
   114  		return blockchain.EVMNetwork{}, InternalDockerUrls{}, err
   115  	}
   116  	httpPort, err := ct.MappedPort(testcontext.Get(g.t), NatPort(TX_GETH_HTTP_PORT))
   117  	if err != nil {
   118  		return blockchain.EVMNetwork{}, InternalDockerUrls{}, err
   119  	}
   120  	wsPort, err := ct.MappedPort(testcontext.Get(g.t), NatPort(TX_GETH_WS_PORT))
   121  	if err != nil {
   122  		return blockchain.EVMNetwork{}, InternalDockerUrls{}, err
   123  	}
   124  
   125  	g.Container = ct
   126  	g.ExternalHttpUrl = FormatHttpUrl(host, httpPort.Port())
   127  	g.InternalHttpUrl = FormatHttpUrl(g.ContainerName, TX_GETH_HTTP_PORT)
   128  	g.ExternalWsUrl = FormatWsUrl(host, wsPort.Port())
   129  	g.InternalWsUrl = FormatWsUrl(g.ContainerName, TX_GETH_WS_PORT)
   130  
   131  	networkConfig := blockchain.SimulatedEVMNetwork
   132  	networkConfig.Name = "geth"
   133  	networkConfig.URLs = []string{g.ExternalWsUrl}
   134  	networkConfig.HTTPURLs = []string{g.ExternalHttpUrl}
   135  
   136  	internalDockerUrls := InternalDockerUrls{
   137  		HttpUrl: g.InternalHttpUrl,
   138  		WsUrl:   g.InternalWsUrl,
   139  	}
   140  
   141  	g.l.Info().Str("containerName", g.ContainerName).
   142  		Str("internalHttpUrl", g.InternalHttpUrl).
   143  		Str("externalHttpUrl", g.ExternalHttpUrl).
   144  		Str("externalWsUrl", g.ExternalWsUrl).
   145  		Str("internalWsUrl", g.InternalWsUrl).
   146  		Msg("Started Geth container")
   147  
   148  	return networkConfig, internalDockerUrls, nil
   149  }
   150  
   151  func (g *Geth) getGethContainerRequest(networks []string) (*tc.ContainerRequest, *keystore.KeyStore, *accounts.Account, error) {
   152  	blocktime := "1"
   153  
   154  	initScriptFile, err := os.CreateTemp("", "init_script")
   155  	if err != nil {
   156  		return nil, nil, nil, err
   157  	}
   158  	_, err = initScriptFile.WriteString(templates.InitGethScript)
   159  	if err != nil {
   160  		return nil, nil, nil, err
   161  	}
   162  	keystoreDir, err := os.MkdirTemp("", "keystore")
   163  	if err != nil {
   164  		return nil, nil, nil, err
   165  	}
   166  	// Create keystore and ethereum account
   167  	ks := keystore.NewKeyStore(keystoreDir, keystore.StandardScryptN, keystore.StandardScryptP)
   168  	account, err := ks.NewAccount("")
   169  	if err != nil {
   170  		return nil, ks, &account, err
   171  	}
   172  	genesisJsonStr, err := templates.GenesisJsonTemplate{
   173  		ChainId:     fmt.Sprintf("%d", g.chainConfig.ChainID),
   174  		AccountAddr: account.Address.Hex(),
   175  	}.String()
   176  	if err != nil {
   177  		return nil, ks, &account, err
   178  	}
   179  	genesisFile, err := os.CreateTemp("", "genesis_json")
   180  	if err != nil {
   181  		return nil, ks, &account, err
   182  	}
   183  	_, err = genesisFile.WriteString(genesisJsonStr)
   184  	if err != nil {
   185  		return nil, ks, &account, err
   186  	}
   187  	key1File, err := os.CreateTemp(keystoreDir, "key1")
   188  	if err != nil {
   189  		return nil, ks, &account, err
   190  	}
   191  	_, err = key1File.WriteString(RootFundingWallet)
   192  	if err != nil {
   193  		return nil, ks, &account, err
   194  	}
   195  	configDir, err := os.MkdirTemp("", "config")
   196  	if err != nil {
   197  		return nil, ks, &account, err
   198  	}
   199  	err = os.WriteFile(configDir+"/password.txt", []byte(""), 0600)
   200  	if err != nil {
   201  		return nil, ks, &account, err
   202  	}
   203  
   204  	return &tc.ContainerRequest{
   205  		Name:            g.ContainerName,
   206  		AlwaysPullImage: true,
   207  		Image:           g.GetImageWithVersion(),
   208  		ExposedPorts:    []string{NatPortFormat(TX_GETH_HTTP_PORT), NatPortFormat(TX_GETH_WS_PORT)},
   209  		Networks:        networks,
   210  		WaitingFor: tcwait.ForAll(
   211  			NewHTTPStrategy("/", NatPort(TX_GETH_HTTP_PORT)),
   212  			tcwait.ForLog("WebSocket enabled"),
   213  			tcwait.ForLog("Started P2P networking").
   214  				WithStartupTimeout(120*time.Second).
   215  				WithPollInterval(1*time.Second),
   216  			NewWebSocketStrategy(NatPort(TX_GETH_WS_PORT), g.l),
   217  		),
   218  		Entrypoint: []string{"sh", "./root/init.sh",
   219  			"--dev",
   220  			"--password", "/root/config/password.txt",
   221  			"--datadir",
   222  			"/root/.ethereum/devchain",
   223  			"--unlock",
   224  			RootFundingAddr,
   225  			"--mine",
   226  			"--miner.etherbase",
   227  			RootFundingAddr,
   228  			"--ipcdisable",
   229  			"--http",
   230  			"--http.vhosts",
   231  			"*",
   232  			"--http.addr",
   233  			"0.0.0.0",
   234  			fmt.Sprintf("--http.port=%s", TX_GETH_HTTP_PORT),
   235  			"--ws",
   236  			"--ws.origins",
   237  			"*",
   238  			"--ws.addr",
   239  			"0.0.0.0",
   240  			"--ws.api", "admin,debug,web3,eth,txpool,personal,clique,miner,net",
   241  			fmt.Sprintf("--ws.port=%s", TX_GETH_WS_PORT),
   242  			"--graphql",
   243  			"-graphql.corsdomain",
   244  			"*",
   245  			"--allow-insecure-unlock",
   246  			"--rpc.allow-unprotected-txs",
   247  			"--http.api",
   248  			"eth,web3,debug",
   249  			"--http.corsdomain",
   250  			"*",
   251  			"--vmdebug",
   252  			fmt.Sprintf("--networkid=%d", g.chainConfig.ChainID),
   253  			"--rpc.txfeecap",
   254  			"0",
   255  			"--dev.period",
   256  			blocktime,
   257  		},
   258  		Files: []tc.ContainerFile{
   259  			{
   260  				HostFilePath:      initScriptFile.Name(),
   261  				ContainerFilePath: "/root/init.sh",
   262  				FileMode:          0644,
   263  			},
   264  			{
   265  				HostFilePath:      genesisFile.Name(),
   266  				ContainerFilePath: "/root/genesis.json",
   267  				FileMode:          0644,
   268  			},
   269  		},
   270  		Mounts: tc.ContainerMounts{
   271  			tc.ContainerMount{
   272  				Source: tc.GenericBindMountSource{
   273  					HostPath: keystoreDir,
   274  				},
   275  				Target: "/root/.ethereum/devchain/keystore/",
   276  			},
   277  			tc.ContainerMount{
   278  				Source: tc.GenericBindMountSource{
   279  					HostPath: configDir,
   280  				},
   281  				Target: "/root/config/",
   282  			},
   283  		},
   284  		LifecycleHooks: []tc.ContainerLifecycleHooks{
   285  			{
   286  				PostStarts: g.PostStartsHooks,
   287  				PostStops:  g.PostStopsHooks,
   288  			},
   289  		},
   290  	}, ks, &account, nil
   291  }
   292  
   293  type WebSocketStrategy struct {
   294  	Port       nat.Port
   295  	RetryDelay time.Duration
   296  	timeout    time.Duration
   297  	l          zerolog.Logger
   298  }
   299  
   300  func NewWebSocketStrategy(port nat.Port, l zerolog.Logger) *WebSocketStrategy {
   301  	return &WebSocketStrategy{
   302  		Port:       port,
   303  		RetryDelay: 10 * time.Second,
   304  		timeout:    2 * time.Minute,
   305  	}
   306  }
   307  
   308  func (w *WebSocketStrategy) WithTimeout(timeout time.Duration) *WebSocketStrategy {
   309  	w.timeout = timeout
   310  	return w
   311  }
   312  
   313  func (w *WebSocketStrategy) WaitUntilReady(ctx context.Context, target tcwait.StrategyTarget) (err error) {
   314  	var client *rpc.Client
   315  	var host string
   316  	ctx, cancel := context.WithTimeout(ctx, w.timeout)
   317  	defer cancel()
   318  	i := 0
   319  	for {
   320  		host, err = GetHost(ctx, target.(tc.Container))
   321  		if err != nil {
   322  			w.l.Error().Msg("Failed to get the target host")
   323  			return err
   324  		}
   325  		wsPort, err := target.MappedPort(ctx, w.Port)
   326  		if err != nil {
   327  			return err
   328  		}
   329  
   330  		url := fmt.Sprintf("ws://%s:%s", host, wsPort.Port())
   331  		w.l.Info().Msgf("Attempting to dial %s", url)
   332  		client, err = rpc.DialContext(ctx, url)
   333  		if err == nil {
   334  			client.Close()
   335  			w.l.Info().Msg("WebSocket rpc port is ready")
   336  			return nil
   337  		}
   338  		if client != nil {
   339  			client.Close() // Close client if DialContext failed
   340  			client = nil
   341  		}
   342  
   343  		select {
   344  		case <-ctx.Done():
   345  			return ctx.Err()
   346  		case <-time.After(w.RetryDelay):
   347  			i++
   348  			w.l.Info().Msgf("WebSocket attempt %d failed: %s. Retrying...", i, err)
   349  		}
   350  	}
   351  }
   352  
   353  type HTTPStrategy struct {
   354  	Path               string
   355  	Port               nat.Port
   356  	RetryDelay         time.Duration
   357  	ExpectedStatusCode int
   358  	timeout            time.Duration
   359  }
   360  
   361  func NewHTTPStrategy(path string, port nat.Port) *HTTPStrategy {
   362  	return &HTTPStrategy{
   363  		Path:               path,
   364  		Port:               port,
   365  		RetryDelay:         10 * time.Second,
   366  		ExpectedStatusCode: 200,
   367  		timeout:            2 * time.Minute,
   368  	}
   369  }
   370  
   371  func (w *HTTPStrategy) WithTimeout(timeout time.Duration) *HTTPStrategy {
   372  	w.timeout = timeout
   373  	return w
   374  }
   375  
   376  func (w *HTTPStrategy) WithStatusCode(statusCode int) *HTTPStrategy {
   377  	w.ExpectedStatusCode = statusCode
   378  	return w
   379  }
   380  
   381  // WaitUntilReady implements Strategy.WaitUntilReady
   382  func (w *HTTPStrategy) WaitUntilReady(ctx context.Context, target tcwait.StrategyTarget) (err error) {
   383  
   384  	ctx, cancel := context.WithTimeout(ctx, w.timeout)
   385  	defer cancel()
   386  
   387  	host, err := GetHost(ctx, target.(tc.Container))
   388  	if err != nil {
   389  		return
   390  	}
   391  
   392  	var mappedPort nat.Port
   393  	mappedPort, err = target.MappedPort(ctx, w.Port)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	tripper := &http.Transport{
   399  		Proxy: http.ProxyFromEnvironment,
   400  		DialContext: (&net.Dialer{
   401  			Timeout:   time.Second,
   402  			KeepAlive: 30 * time.Second,
   403  			DualStack: true,
   404  		}).DialContext,
   405  		ForceAttemptHTTP2:     true,
   406  		MaxIdleConns:          100,
   407  		IdleConnTimeout:       90 * time.Second,
   408  		ExpectContinueTimeout: 1 * time.Second,
   409  	}
   410  
   411  	client := http.Client{Transport: tripper, Timeout: time.Second}
   412  	address := net.JoinHostPort(host, strconv.Itoa(mappedPort.Int()))
   413  
   414  	endpoint := url.URL{
   415  		Scheme: "http",
   416  		Host:   address,
   417  		Path:   w.Path,
   418  	}
   419  
   420  	var body []byte
   421  	for {
   422  		select {
   423  		case <-ctx.Done():
   424  			return ctx.Err()
   425  		case <-time.After(100 * time.Millisecond):
   426  			state, err := target.State(ctx)
   427  			if err != nil {
   428  				return err
   429  			}
   430  			if !state.Running {
   431  				return fmt.Errorf("container is not running %s", state.Status)
   432  			}
   433  			req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), bytes.NewReader(body))
   434  			if err != nil {
   435  				return err
   436  			}
   437  			resp, err := client.Do(req)
   438  			if err != nil {
   439  				continue
   440  			}
   441  			if resp.StatusCode != w.ExpectedStatusCode {
   442  				_ = resp.Body.Close()
   443  				continue
   444  			}
   445  			if err := resp.Body.Close(); err != nil {
   446  				continue
   447  			}
   448  			return nil
   449  		}
   450  	}
   451  }