github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/docker/test_env/non_dev_besu.go (about) 1 package test_env 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "io" 7 "os" 8 "strconv" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/ethereum/go-ethereum/accounts/keystore" 14 "github.com/ethereum/go-ethereum/common" 15 "github.com/ethereum/go-ethereum/ethclient" 16 "github.com/google/uuid" 17 "github.com/rs/zerolog" 18 tc "github.com/testcontainers/testcontainers-go" 19 tcwait "github.com/testcontainers/testcontainers-go/wait" 20 21 "github.com/smartcontractkit/chainlink-testing-framework/libs/blockchain" 22 "github.com/smartcontractkit/chainlink-testing-framework/libs/logging" 23 "github.com/smartcontractkit/chainlink-testing-framework/libs/mirror" 24 "github.com/smartcontractkit/chainlink-testing-framework/libs/utils/templates" 25 "github.com/smartcontractkit/chainlink-testing-framework/libs/utils/testcontext" 26 ) 27 28 const ( 29 BESU_IMAGE = "hyperledger/besu" 30 ) 31 32 type PrivateBesuChain struct { 33 PrimaryNode *NonDevBesuNode 34 Nodes []*NonDevBesuNode 35 NetworkConfig *blockchain.EVMNetwork 36 DockerNetworks []string 37 } 38 39 func NewPrivateBesuChain(networkCfg *blockchain.EVMNetwork, dockerNetworks []string) PrivateChain { 40 evmChain := &PrivateBesuChain{ 41 NetworkConfig: networkCfg, 42 DockerNetworks: dockerNetworks, 43 } 44 evmChain.PrimaryNode = NewNonDevBesuNode(dockerNetworks, networkCfg) 45 evmChain.Nodes = []*NonDevBesuNode{evmChain.PrimaryNode} 46 return evmChain 47 } 48 49 func (p *PrivateBesuChain) GetPrimaryNode() NonDevNode { 50 return p.PrimaryNode 51 } 52 53 func (p *PrivateBesuChain) GetNodes() []NonDevNode { 54 nodes := make([]NonDevNode, 0) 55 for _, node := range p.Nodes { 56 nodes = append(nodes, node) 57 } 58 return nodes 59 } 60 61 func (p *PrivateBesuChain) GetNetworkConfig() *blockchain.EVMNetwork { 62 return p.NetworkConfig 63 } 64 65 func (p *PrivateBesuChain) GetDockerNetworks() []string { 66 return p.DockerNetworks 67 } 68 69 type NonDevBesuNode struct { 70 EnvComponent 71 Config gethTxNodeConfig 72 ExternalHttpUrl string 73 InternalHttpUrl string 74 ExternalWsUrl string 75 InternalWsUrl string 76 EVMClient blockchain.EVMClient 77 EthClient *ethclient.Client 78 t *testing.T 79 l zerolog.Logger 80 } 81 82 func NewNonDevBesuNode(networks []string, networkCfg *blockchain.EVMNetwork) *NonDevBesuNode { 83 n := &NonDevBesuNode{ 84 Config: gethTxNodeConfig{ 85 chainId: strconv.FormatInt(networkCfg.ChainID, 10), 86 networkCfg: networkCfg, 87 }, 88 EnvComponent: EnvComponent{ 89 ContainerName: fmt.Sprintf("%s-%s", 90 strings.ReplaceAll(networkCfg.Name, " ", "_"), uuid.NewString()[0:3]), 91 Networks: networks, 92 }, 93 } 94 n.SetDefaultHooks() 95 96 return n 97 } 98 99 func (g *NonDevBesuNode) WithTestInstance(t *testing.T) NonDevNode { 100 g.t = t 101 g.l = logging.GetTestLogger(t) 102 return g 103 } 104 105 func (g *NonDevBesuNode) GetInternalHttpUrl() string { 106 return g.InternalHttpUrl 107 } 108 109 func (g *NonDevBesuNode) GetInternalWsUrl() string { 110 return g.InternalWsUrl 111 } 112 113 func (g *NonDevBesuNode) GetEVMClient() blockchain.EVMClient { 114 return g.EVMClient 115 } 116 117 func (g *NonDevBesuNode) createMountDirs() error { 118 keystorePath, err := os.MkdirTemp("", "keystore") 119 if err != nil { 120 return err 121 } 122 g.Config.keystorePath = keystorePath 123 124 // Create keystore and ethereum account 125 ks := keystore.NewKeyStore(g.Config.keystorePath, keystore.StandardScryptN, keystore.StandardScryptP) 126 account, err := ks.NewAccount("") 127 if err != nil { 128 return err 129 } 130 131 g.Config.accountAddr = account.Address.Hex() 132 addr := strings.Replace(account.Address.Hex(), "0x", "", 1) 133 FundingAddresses[addr] = "" 134 signerBytes, err := hex.DecodeString(addr) 135 if err != nil { 136 fmt.Println("Error decoding signer address:", err) 137 return err 138 } 139 140 zeroBytes := make([]byte, 32) // Create 32 zero bytes 141 extradata := append(zeroBytes, signerBytes...) // Concatenate zero bytes and signer address 142 extradata = append(extradata, make([]byte, 65)...) // Concatenate 65 more zero bytes 143 144 fmt.Printf("Encoded extradata: 0x%s\n", hex.EncodeToString(extradata)) 145 146 i := 1 147 var accounts []string 148 for addr, v := range FundingAddresses { 149 if v == "" { 150 continue 151 } 152 f, err := os.Create(fmt.Sprintf("%s/%s", g.Config.keystorePath, fmt.Sprintf("key%d", i))) 153 if err != nil { 154 return err 155 } 156 _, err = f.WriteString(v) 157 if err != nil { 158 return err 159 } 160 i++ 161 accounts = append(accounts, addr) 162 } 163 err = os.WriteFile(g.Config.keystorePath+"/password.txt", []byte(""), 0600) 164 if err != nil { 165 return err 166 } 167 168 genesisJsonStr, err := templates.BuildBesuGenesisJsonForNonDevChain(g.Config.chainId, 169 accounts, 170 fmt.Sprintf("0x%s", hex.EncodeToString(extradata))) 171 if err != nil { 172 return err 173 } 174 f, err := os.CreateTemp("", "genesis_json") 175 if err != nil { 176 return err 177 } 178 defer f.Close() 179 _, err = f.WriteString(genesisJsonStr) 180 if err != nil { 181 return err 182 } 183 184 g.Config.genesisPath = f.Name() 185 186 configDir, err := os.MkdirTemp("", "config") 187 if err != nil { 188 return err 189 } 190 g.Config.rootPath = configDir 191 192 return nil 193 } 194 195 func (g *NonDevBesuNode) ConnectToClient() error { 196 ct := g.Container 197 if ct == nil { 198 return fmt.Errorf("container not started") 199 } 200 host, err := GetHost(testcontext.Get(g.t), ct) 201 if err != nil { 202 return err 203 } 204 port := NatPort(TX_GETH_HTTP_PORT) 205 httpPort, err := ct.MappedPort(testcontext.Get(g.t), port) 206 if err != nil { 207 return err 208 } 209 port = NatPort(TX_NON_DEV_GETH_WS_PORT) 210 wsPort, err := ct.MappedPort(testcontext.Get(g.t), port) 211 if err != nil { 212 return err 213 } 214 g.ExternalHttpUrl = fmt.Sprintf("http://%s:%s", host, httpPort.Port()) 215 g.InternalHttpUrl = fmt.Sprintf("http://%s:%s", g.ContainerName, TX_GETH_HTTP_PORT) 216 g.ExternalWsUrl = fmt.Sprintf("ws://%s:%s", host, wsPort.Port()) 217 g.InternalWsUrl = fmt.Sprintf("ws://%s:%s", g.ContainerName, TX_NON_DEV_GETH_WS_PORT) 218 219 networkConfig := g.Config.networkCfg 220 networkConfig.URLs = []string{g.ExternalWsUrl} 221 networkConfig.HTTPURLs = []string{g.ExternalHttpUrl} 222 223 ec, err := blockchain.NewEVMClientFromNetwork(*networkConfig, g.l) 224 if err != nil { 225 return err 226 } 227 at, err := ec.BalanceAt(testcontext.Get(g.t), common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) 228 if err != nil { 229 return err 230 } 231 fmt.Printf("balance: %s\n", at.String()) 232 g.EVMClient = ec 233 // to make sure all the pending txs are done 234 err = ec.WaitForEvents() 235 if err != nil { 236 return err 237 } 238 switch val := ec.(type) { 239 case *blockchain.EthereumMultinodeClient: 240 ethClient, ok := val.Clients[0].(*blockchain.EthereumClient) 241 if !ok { 242 return fmt.Errorf("could not get blockchain.EthereumClient from %+v", val) 243 } 244 g.EthClient = ethClient.Client 245 default: 246 return fmt.Errorf("%+v not supported for geth", val) 247 } 248 if err != nil { 249 return err 250 } 251 return nil 252 } 253 254 func (g *NonDevBesuNode) Start() error { 255 err := g.createMountDirs() 256 if err != nil { 257 return err 258 } 259 l := logging.GetTestContainersGoTestLogger(g.t) 260 261 // Besu Bootnode setup: BEGIN 262 // Generate public key for besu bootnode 263 crbn, err := g.getBesuBootNodeContainerRequest() 264 if err != nil { 265 return err 266 } 267 bootNode, err := tc.GenericContainer(testcontext.Get(g.t), 268 tc.GenericContainerRequest{ 269 ContainerRequest: crbn, 270 Started: true, 271 Reuse: true, 272 Logger: l, 273 }) 274 if err != nil { 275 return err 276 } 277 278 err = g.exportBesuBootNodeAddress(bootNode) 279 if err != nil { 280 return err 281 } 282 // Besu Bootnode setup: END 283 284 host, err := GetHost(testcontext.Get(g.t), bootNode) 285 if err != nil { 286 return err 287 } 288 r, err := bootNode.CopyFileFromContainer(testcontext.Get(g.t), "/opt/besu/nodedata/bootnodes") 289 if err != nil { 290 return err 291 } 292 defer r.Close() 293 b, err := io.ReadAll(r) 294 if err != nil { 295 return err 296 } 297 bootnodePubKey := strings.TrimPrefix(strings.TrimSpace(string(b)), "0x") 298 g.Config.bootNodeURL = fmt.Sprintf("enode://%s@%s:%s", bootnodePubKey, host, BOOTNODE_PORT) 299 300 fmt.Printf("Besu Bootnode URL: %s\n", g.Config.bootNodeURL) 301 302 cr, err := g.getBesuContainerRequest() 303 if err != nil { 304 return err 305 } 306 var ct tc.Container 307 ct, err = tc.GenericContainer(testcontext.Get(g.t), 308 tc.GenericContainerRequest{ 309 ContainerRequest: cr, 310 Started: true, 311 Reuse: true, 312 }) 313 if err != nil { 314 return err 315 } 316 g.Container = ct 317 return nil 318 } 319 320 func (g *NonDevBesuNode) getBesuBootNodeContainerRequest() (tc.ContainerRequest, error) { 321 besuImage, err := mirror.GetImage(BESU_IMAGE) 322 if err != nil { 323 return tc.ContainerRequest{}, err 324 } 325 return tc.ContainerRequest{ 326 Name: g.ContainerName + "-bootnode", 327 Image: besuImage, 328 Networks: g.Networks, 329 ExposedPorts: []string{"30301/udp"}, 330 WaitingFor: tcwait.ForLog("PeerDiscoveryAgent | P2P peer discovery agent started and listening on"). 331 WithStartupTimeout(999 * time.Second). 332 WithPollInterval(1 * time.Second), 333 Cmd: []string{ 334 "--genesis-file", 335 "/opt/besu/nodedata/genesis.json", 336 "--data-path", 337 "/opt/besu/nodedata", 338 "--logging=INFO", 339 "--p2p-port=30301", 340 "--bootnodes", 341 }, 342 Files: []tc.ContainerFile{ 343 { 344 HostFilePath: g.Config.genesisPath, 345 ContainerFilePath: "/opt/besu/nodedata/genesis.json", 346 FileMode: 0644, 347 }, 348 }, 349 Mounts: tc.ContainerMounts{ 350 tc.ContainerMount{ 351 Source: tc.GenericBindMountSource{ 352 HostPath: g.Config.rootPath, 353 }, 354 Target: "/opt/besu/nodedata/", 355 }, 356 }, 357 LifecycleHooks: []tc.ContainerLifecycleHooks{ 358 { 359 PostStarts: g.PostStartsHooks, 360 PostStops: g.PostStopsHooks, 361 }, 362 }, 363 }, nil 364 } 365 366 func (g *NonDevBesuNode) exportBesuBootNodeAddress(bootNode tc.Container) (err error) { 367 resCode, _, err := bootNode.Exec(testcontext.Get(g.t), []string{ 368 "besu", 369 "--genesis-file", "/opt/besu/nodedata/genesis.json", 370 "--data-path", "/opt/besu/nodedata", 371 "public-key", "export", 372 "--to=/opt/besu/nodedata/bootnodes", 373 }) 374 fmt.Printf("Export besu bootnode address, process exitcode: %d\n", resCode) 375 if err != nil { 376 return err 377 } 378 return nil 379 } 380 381 func (g *NonDevBesuNode) getBesuContainerRequest() (tc.ContainerRequest, error) { 382 besuImage, err := mirror.GetImage(BESU_IMAGE) 383 if err != nil { 384 return tc.ContainerRequest{}, err 385 } 386 return tc.ContainerRequest{ 387 Name: g.ContainerName, 388 Image: besuImage, 389 ExposedPorts: []string{ 390 NatPortFormat(TX_GETH_HTTP_PORT), 391 NatPortFormat(TX_NON_DEV_GETH_WS_PORT), 392 "30303/tcp", "30303/udp"}, 393 Networks: g.Networks, 394 WaitingFor: tcwait.ForAll( 395 tcwait.ForLog("WebSocketService | Websocket service started"), 396 NewWebSocketStrategy(NatPort(TX_NON_DEV_GETH_WS_PORT), g.l), 397 NewHTTPStrategy("/", NatPort(TX_GETH_HTTP_PORT)).WithStatusCode(201), 398 ), 399 Entrypoint: []string{ 400 "besu", 401 "--genesis-file", "/opt/besu/nodedata/genesis.json", 402 "--host-allowlist", "*", 403 // "--sync-mode", "X_SNAP", // Requires at least 5 peers in X_SNAP mode 404 fmt.Sprintf("--bootnodes=%s", g.Config.bootNodeURL), 405 "--rpc-http-enabled", 406 "--rpc-http-cors-origins", "*", 407 "--rpc-http-api", "ADMIN,DEBUG,WEB3,ETH,TXPOOL,CLIQUE,MINER,NET", 408 "--rpc-http-host", "0.0.0.0", 409 fmt.Sprintf("--rpc-http-port=%s", TX_GETH_HTTP_PORT), 410 "--rpc-ws-enabled", 411 "--rpc-ws-api", "ADMIN,DEBUG,WEB3,ETH,TXPOOL,CLIQUE,MINER,NET", 412 "--rpc-ws-host", "0.0.0.0", 413 fmt.Sprintf("--rpc-ws-port=%s", TX_NON_DEV_GETH_WS_PORT), 414 "--miner-enabled=true", 415 "--miner-coinbase", g.Config.accountAddr, 416 fmt.Sprintf("--network-id=%s", g.Config.chainId), 417 "--logging=DEBUG", 418 }, 419 Files: []tc.ContainerFile{ 420 { 421 HostFilePath: g.Config.genesisPath, 422 ContainerFilePath: "/opt/besu/nodedata/genesis.json", 423 FileMode: 0644, 424 }, 425 }, 426 Mounts: tc.ContainerMounts{ 427 tc.ContainerMount{ 428 Source: tc.GenericBindMountSource{ 429 HostPath: g.Config.keystorePath, 430 }, 431 Target: "/opt/besu/nodedata/keystore/", 432 }, 433 tc.ContainerMount{ 434 Source: tc.GenericBindMountSource{ 435 HostPath: g.Config.rootPath, 436 }, 437 Target: "/opt/besu/nodedata/", 438 }, 439 }, 440 LifecycleHooks: []tc.ContainerLifecycleHooks{ 441 { 442 PostStarts: g.PostStartsHooks, 443 PostStops: g.PostStopsHooks, 444 }, 445 }, 446 }, nil 447 }