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  }