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 }