From 6f4f5777b3c64363b4e6ab7b17324093d18c1d02 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 31 May 2024 02:01:01 +0000 Subject: [PATCH 01/22] Add environment vars cli flag to stack init Signed-off-by: rodion --- cmd/init.go | 1 + .../blockchain/ethereum/besu/besu_provider.go | 3 +- .../ethereum/connector/ethconnect/docker.go | 3 +- .../connector/ethconnect/docker_test.go | 13 ++++ .../ethereum/connector/evmconnect/docker.go | 3 +- .../connector/evmconnect/docker_test.go | 20 +++++- .../ethereum/ethsigner/ethsigner.go | 3 +- .../blockchain/ethereum/geth/geth_provider.go | 1 + internal/blockchain/fabric/fabric_docker.go | 12 ++-- internal/blockchain/fabric/fabric_provider.go | 3 +- .../tezos/connector/tezosconnect/docker.go | 3 +- .../tezos/tezossigner/tezossigner.go | 1 + internal/docker/docker_config.go | 23 +++--- internal/docker/docker_config_test.go | 54 ++++++++++++++ internal/stacks/stack_manager.go | 5 ++ internal/tokens/erc1155/erc1155_provider.go | 4 +- .../erc20erc721/erc20_erc721_provider.go | 4 +- pkg/types/options.go | 1 + pkg/types/stack.go | 70 +++++++++++-------- 19 files changed, 172 insertions(+), 55 deletions(-) create mode 100644 internal/docker/docker_config_test.go diff --git a/cmd/init.go b/cmd/init.go index e5f02df2..277de5b2 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -271,5 +271,6 @@ func init() { initCmd.PersistentFlags().StringArrayVar(&initOptions.OrgNames, "org-name", []string{}, "Organization name") initCmd.PersistentFlags().StringArrayVar(&initOptions.NodeNames, "node-name", []string{}, "Node name") initCmd.PersistentFlags().BoolVar(&initOptions.RemoteNodeDeploy, "remote-node-deploy", false, "Enable or disable deployment of FireFly contracts on remote nodes") + initCmd.PersistentFlags().StringToStringVar(&initOptions.EnvironmentVars, "environment-vars", map[string]string{}, "Common environment variables to set on all containers in FireFly stack") rootCmd.AddCommand(initCmd) } diff --git a/internal/blockchain/ethereum/besu/besu_provider.go b/internal/blockchain/ethereum/besu/besu_provider.go index 261594e7..158e075d 100644 --- a/internal/blockchain/ethereum/besu/besu_provider.go +++ b/internal/blockchain/ethereum/besu/besu_provider.go @@ -172,7 +172,8 @@ func (p *BesuProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition Volumes: []string{ "besu:/data", }, - Logging: docker.StandardLogOptions, + Logging: docker.StandardLogOptions, + Environment: p.stack.EnvironmentVars, }, VolumeNames: []string{"besu"}, diff --git a/internal/blockchain/ethereum/connector/ethconnect/docker.go b/internal/blockchain/ethereum/connector/ethconnect/docker.go index f04448d9..55858098 100644 --- a/internal/blockchain/ethereum/connector/ethconnect/docker.go +++ b/internal/blockchain/ethereum/connector/ethconnect/docker.go @@ -42,7 +42,8 @@ func (e *Ethconnect) GetServiceDefinitions(s *types.Stack, dependentServices map fmt.Sprintf("ethconnect_config_%s:/ethconnect/config", member.ID), fmt.Sprintf("ethconnect_data_%s:/ethconnect/data", member.ID), }, - Logging: docker.StandardLogOptions, + Logging: docker.StandardLogOptions, + Environment: s.EnvironmentVars, }, VolumeNames: []string{ fmt.Sprintf("ethconnect_config_%v", member.ID), diff --git a/internal/blockchain/ethereum/connector/ethconnect/docker_test.go b/internal/blockchain/ethereum/connector/ethconnect/docker_test.go index 044bbb6c..fec8444b 100644 --- a/internal/blockchain/ethereum/connector/ethconnect/docker_test.go +++ b/internal/blockchain/ethereum/connector/ethconnect/docker_test.go @@ -75,6 +75,19 @@ func TestGetServiceDefinition(t *testing.T) { }, ServiceName: "ethconnect_firefly_4", }, + { + Name: "test_env_vars_5", + Members: &types.Stack{ + Members: []*types.Organization{{ID: "firefly_5", ExposedConnectorPort: 7892}}, + VersionManifest: &types.VersionManifest{Ethconnect: &getManifest.ManifestEntry}, + EnvironmentVars: map[string]interface{}{"HTTP_PROXY": ""}, + }, + DependentServices: map[string]string{ + "service1": "running", + "service2": "stopped", + }, + ServiceName: "ethconnect_firefly_5", + }, } for _, tc := range testServices { t.Run(tc.Name, func(t *testing.T) { diff --git a/internal/blockchain/ethereum/connector/evmconnect/docker.go b/internal/blockchain/ethereum/connector/evmconnect/docker.go index 07ad626c..99675f30 100644 --- a/internal/blockchain/ethereum/connector/evmconnect/docker.go +++ b/internal/blockchain/ethereum/connector/evmconnect/docker.go @@ -43,7 +43,8 @@ func (e *Evmconnect) GetServiceDefinitions(s *types.Stack, dependentServices map fmt.Sprintf("%s/config/evmconnect_%s.yaml:/evmconnect/config.yaml", s.RuntimeDir, member.ID), fmt.Sprintf("%s:/evmconnect/data", dataVolumeName), }, - Logging: docker.StandardLogOptions, + Logging: docker.StandardLogOptions, + Environment: s.EnvironmentVars, }, VolumeNames: []string{ dataVolumeName, diff --git a/internal/blockchain/ethereum/connector/evmconnect/docker_test.go b/internal/blockchain/ethereum/connector/evmconnect/docker_test.go index 52bb2199..c872d4d4 100644 --- a/internal/blockchain/ethereum/connector/evmconnect/docker_test.go +++ b/internal/blockchain/ethereum/connector/evmconnect/docker_test.go @@ -75,6 +75,19 @@ func TestGetServiceDefinition(t *testing.T) { }, ServiceName: "evmconnect_firefly_4", }, + { + Name: "test_env_vars_5", + Members: &types.Stack{ + Members: []*types.Organization{{ID: "firefly_5", ExposedConnectorPort: 7892}}, + VersionManifest: &types.VersionManifest{Evmconnect: &getManifest.ManifestEntry}, + EnvironmentVars: map[string]interface{}{"HTTP_PROXY": ""}, + }, + DependentServices: map[string]string{ + "service1": "running", + "service2": "stopped", + }, + ServiceName: "evmconnect_firefly_5", + }, } for _, tc := range testServices { t.Run(tc.Name, func(t *testing.T) { @@ -90,7 +103,12 @@ func TestGetServiceDefinition(t *testing.T) { if serviceDefinitions[0].ServiceName != tc.ServiceName { t.Errorf("Expected ServiceName %q, got %q", tc.ServiceName, serviceDefinitions[0].ServiceName) } - + if len(tc.Members.EnvironmentVars) != len(serviceDefinitions[0].Service.Environment) { + t.Errorf("Expected EnvironmentVars %q, got %q", tc.Members.EnvironmentVars, serviceDefinitions[0].Service.Environment) + } + for k := range tc.Members.EnvironmentVars { + assert.Equal(t, tc.Members.EnvironmentVars[k], serviceDefinitions[0].Service.Environment[k]) + } }) } diff --git a/internal/blockchain/ethereum/ethsigner/ethsigner.go b/internal/blockchain/ethereum/ethsigner/ethsigner.go index 07098a73..643166f2 100644 --- a/internal/blockchain/ethereum/ethsigner/ethsigner.go +++ b/internal/blockchain/ethereum/ethsigner/ethsigner.go @@ -160,7 +160,8 @@ func (p *EthSignerProvider) GetDockerServiceDefinition(rpcURL string) *docker.Se Interval: "15s", // 6000 requests in a day Retries: 60, }, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort)}, + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort)}, + Environment: p.stack.EnvironmentVars, }, VolumeNames: []string{ "ethsigner", diff --git a/internal/blockchain/ethereum/geth/geth_provider.go b/internal/blockchain/ethereum/geth/geth_provider.go index 9acead70..9b5a8af5 100644 --- a/internal/blockchain/ethereum/geth/geth_provider.go +++ b/internal/blockchain/ethereum/geth/geth_provider.go @@ -190,6 +190,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition Volumes: []string{"geth:/data"}, Logging: docker.StandardLogOptions, Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort)}, + Environment: p.stack.EnvironmentVars, }, VolumeNames: []string{"geth"}, } diff --git a/internal/blockchain/fabric/fabric_docker.go b/internal/blockchain/fabric/fabric_docker.go index 2ab71143..78efbd8e 100644 --- a/internal/blockchain/fabric/fabric_docker.go +++ b/internal/blockchain/fabric/fabric_docker.go @@ -31,14 +31,14 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricCAImageName, ContainerName: fmt.Sprintf("%s_fabric_ca", s.Name), - Environment: map[string]interface{}{ + Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ "FABRIC_CA_HOME": "/etc/hyperledger/fabric-ca-server", "FABRIC_CA_SERVER_CA_NAME": "fabric_ca", "FABRIC_CA_SERVER_PORT": "7054", "FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS": "0.0.0.0:17054", "FABRIC_CA_SERVER_CA_CERTFILE": "/etc/firefly/organizations/peerOrganizations/org1.example.com/ca/fabric_ca.org1.example.com-cert.pem", "FABRIC_CA_SERVER_CA_KEYFILE": "/etc/firefly/organizations/peerOrganizations/org1.example.com/ca/priv_sk", - }, + }), Ports: []string{ "7054:7054", "17054:17054", @@ -57,7 +57,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricOrdererImageName, ContainerName: fmt.Sprintf("%s_fabric_orderer", s.Name), - Environment: map[string]interface{}{ + Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ "FABRIC_LOGGING_SPEC": "INFO", "ORDERER_GENERAL_LISTENADDRESS": "0.0.0.0", "ORDERER_GENERAL_LISTENPORT": "7050", @@ -81,7 +81,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio "ORDERER_ADMIN_TLS_CLIENTROOTCAS": "[/etc/firefly/organizations/ordererOrganizations/example.com/orderers/fabric_orderer.example.com/tls/ca.crt]", "ORDERER_ADMIN_LISTENADDRESS": "0.0.0.0:7053", "ORDERER_OPERATIONS_LISTENADDRESS": "0.0.0.0:17050", - }, + }), WorkingDir: "/opt/gopath/src/github.com/hyperledger/fabric", Command: "orderer", Volumes: []string{ @@ -103,7 +103,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricPeerImageName, ContainerName: fmt.Sprintf("%s_fabric_peer", s.Name), - Environment: map[string]interface{}{ + Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ "CORE_VM_ENDPOINT": "unix:///host/var/run/docker.sock", "CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE": fmt.Sprintf("%s_default", s.Name), "FABRIC_LOGGING_SPEC": "INFO", @@ -122,7 +122,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio "CORE_PEER_GOSSIP_EXTERNALENDPOINT": "fabric_peer:7051", "CORE_PEER_LOCALMSPID": "Org1MSP", "CORE_OPERATIONS_LISTENADDRESS": "0.0.0.0:17051", - }, + }), Volumes: []string{ "firefly_fabric:/etc/firefly", "fabric_peer:/var/hyperledger/production", diff --git a/internal/blockchain/fabric/fabric_provider.go b/internal/blockchain/fabric/fabric_provider.go index 8bac72dd..e5828ae1 100644 --- a/internal/blockchain/fabric/fabric_provider.go +++ b/internal/blockchain/fabric/fabric_provider.go @@ -290,7 +290,8 @@ func (p *FabricProvider) getFabconnectServiceDefinitions(members []*types.Organi HealthCheck: &docker.HealthCheck{ Test: []string{"CMD", "wget", "-O", "-", "http://localhost:3000/status"}, }, - Logging: docker.StandardLogOptions, + Logging: docker.StandardLogOptions, + Environment: p.stack.EnvironmentVars, }, VolumeNames: []string{ "fabconnect_receipts_" + member.ID, diff --git a/internal/blockchain/tezos/connector/tezosconnect/docker.go b/internal/blockchain/tezos/connector/tezosconnect/docker.go index 4770d1ef..7fa902fa 100644 --- a/internal/blockchain/tezos/connector/tezosconnect/docker.go +++ b/internal/blockchain/tezos/connector/tezosconnect/docker.go @@ -42,7 +42,8 @@ func (t *Tezosconnect) GetServiceDefinitions(s *types.Stack, dependentServices m fmt.Sprintf("tezosconnect_config_%s:/tezosconnect/config", member.ID), fmt.Sprintf("tezosconnect_leveldb_%s:/tezosconnect/leveldb", member.ID), }, - Logging: docker.StandardLogOptions, + Logging: docker.StandardLogOptions, + Environment: s.EnvironmentVars, }, VolumeNames: []string{ fmt.Sprintf("tezosconnect_config_%s", member.ID), diff --git a/internal/blockchain/tezos/tezossigner/tezossigner.go b/internal/blockchain/tezos/tezossigner/tezossigner.go index dae114ed..ab18ec36 100644 --- a/internal/blockchain/tezos/tezossigner/tezossigner.go +++ b/internal/blockchain/tezos/tezossigner/tezossigner.go @@ -113,6 +113,7 @@ func (p *TezosSignerProvider) GetDockerServiceDefinition(rpcURL string) *docker. fmt.Sprintf("%d:6732", p.stack.ExposedBlockchainPort), "9583:9583", }, + Environment: p.stack.EnvironmentVars, }, VolumeNames: []string{ "tezossigner", diff --git a/internal/docker/docker_config.go b/internal/docker/docker_config.go index 8a81ca00..06aeff96 100644 --- a/internal/docker/docker_config.go +++ b/internal/docker/docker_config.go @@ -99,8 +99,9 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { fmt.Sprintf("%s:/etc/firefly/firefly.core.yml:ro", configFile), fmt.Sprintf("%s_data_%s:/etc/firefly/data", fireflyCore, member.ID), }, - DependsOn: map[string]map[string]string{}, - Logging: StandardLogOptions, + DependsOn: map[string]map[string]string{}, + Logging: StandardLogOptions, + Environment: s.EnvironmentVars, } compose.Volumes[fmt.Sprintf("%s_data_%s", fireflyCore, member.ID)] = struct{}{} compose.Services[fireflyCore+"_"+member.ID].DependsOn["dataexchange_"+member.ID] = map[string]string{"condition": "service_started"} @@ -111,10 +112,9 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Image: constants.PostgresImageName, ContainerName: fmt.Sprintf("%s_postgres_%s", s.Name, member.ID), Ports: []string{fmt.Sprintf("%d:5432", member.ExposedDatabasePort)}, - Environment: map[string]interface{}{ + Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ "POSTGRES_PASSWORD": "f1refly", - "PGDATA": "/var/lib/postgresql/data/pgdata", - }, + "PGDATA": "/var/lib/postgresql/data/pgdata"}), Volumes: []string{fmt.Sprintf("postgres_%s:/var/lib/postgresql/data", member.ID)}, HealthCheck: &HealthCheck{ Test: []string{"CMD-SHELL", "pg_isready -U postgres"}, @@ -149,10 +149,13 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { }, } if s.IPFSMode.Equals(types.IPFSModePrivate) { - sharedStorage.Environment = map[string]interface{}{ + sharedStorage.Environment = s.ConcatenateEnvironmentVars(map[string]interface{}{ "IPFS_SWARM_KEY": s.SwarmKey, "LIBP2P_FORCE_PNET": "1", - } + }, + ) + } else { + sharedStorage.Environment = s.EnvironmentVars } compose.Services["ipfs_"+member.ID] = sharedStorage compose.Volumes[fmt.Sprintf("ipfs_staging_%s", member.ID)] = struct{}{} @@ -163,6 +166,7 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Ports: []string{fmt.Sprintf("%d:3000", member.ExposedDataexchangePort)}, Volumes: []string{fmt.Sprintf("dataexchange_%s:/data", member.ID)}, Logging: StandardLogOptions, + Environment: s.EnvironmentVars, } compose.Volumes[fmt.Sprintf("dataexchange_%s", member.ID)] = struct{}{} if s.SandboxEnabled { @@ -170,9 +174,9 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Image: constants.SandboxImageName, ContainerName: fmt.Sprintf("%s_sandbox_%s", s.Name, member.ID), Ports: []string{fmt.Sprintf("%d:3001", member.ExposedSandboxPort)}, - Environment: map[string]interface{}{ + Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ "FF_ENDPOINT": fmt.Sprintf("http://firefly_core_%d:%d", *member.Index, member.ExposedFireflyPort), - }, + }), } } } @@ -184,6 +188,7 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Ports: []string{fmt.Sprintf("%d:9090", s.ExposedPrometheusPort)}, Volumes: []string{"prometheus_data:/prometheus", "prometheus_config:/etc/prometheus"}, Logging: StandardLogOptions, + Environment: s.EnvironmentVars, } compose.Volumes["prometheus_data"] = struct{}{} compose.Volumes["prometheus_config"] = struct{}{} diff --git a/internal/docker/docker_config_test.go b/internal/docker/docker_config_test.go new file mode 100644 index 00000000..b2acdcfb --- /dev/null +++ b/internal/docker/docker_config_test.go @@ -0,0 +1,54 @@ +package docker + +import ( + "testing" + + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/stretchr/testify/assert" +) + +type MockManfest struct { + types.ManifestEntry +} + +func TestCreateDockerComposeEnvironmentVars(t *testing.T) { + getManifest := &MockManfest{} + testStacks := []struct { + Id int + EnvironmentVars map[string]interface{} + Members []*types.Organization + VersionManifest *types.VersionManifest + }{ + { + Id: 1, + Members: []*types.Organization{{ID: "firefly_1"}}, + VersionManifest: &types.VersionManifest{FireFly: &getManifest.ManifestEntry, DataExchange: &getManifest.ManifestEntry}, + EnvironmentVars: map[string]interface{}{ + "http_proxy": "", + "https_proxy": "", + "HTTP_PROXY": "127.0.0.1:8080", + "HTTPS_PROXY": "127.0.0.1:8080", + "no_proxy": "", + }, + }, + { + Id: 2, + Members: []*types.Organization{{ID: "firefly_2"}}, + VersionManifest: &types.VersionManifest{FireFly: &getManifest.ManifestEntry, DataExchange: &getManifest.ManifestEntry}, + EnvironmentVars: nil, + }, + } + for _, test := range testStacks { + cfg := CreateDockerCompose(&types.Stack{ + Members: test.Members, + VersionManifest: test.VersionManifest, + EnvironmentVars: test.EnvironmentVars, + }) + for _, service := range cfg.Services { + assert.Equal(t, len(test.EnvironmentVars), len(service.Environment), "service [%v] test ID [%v]", service.ContainerName, test.Id) + for envVar := range service.Environment { + assert.Equal(t, test.EnvironmentVars[envVar], service.Environment[envVar]) + } + } + } +} diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index ccd4e2e7..b1075b53 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -88,6 +88,10 @@ func NewStackManager(ctx context.Context) *StackManager { } func (s *StackManager) InitStack(options *types.InitOptions) (err error) { + environmentVarsMap := make(map[string]interface{}) + for key, value := range options.EnvironmentVars { + environmentVarsMap[key] = value + } s.Stack = &types.Stack{ Name: options.StackName, Members: make([]*types.Organization, options.MemberCount), @@ -114,6 +118,7 @@ func (s *StackManager) InitStack(options *types.InitOptions) (err error) { ChaincodeName: options.ChaincodeName, CustomPinSupport: options.CustomPinSupport, RemoteNodeDeploy: options.RemoteNodeDeploy, + EnvironmentVars: environmentVarsMap, } tokenProviders, err := types.FFEnumArray(s.ctx, options.TokenProviders) diff --git a/internal/tokens/erc1155/erc1155_provider.go b/internal/tokens/erc1155/erc1155_provider.go index f8ee7948..61e3499d 100644 --- a/internal/tokens/erc1155/erc1155_provider.go +++ b/internal/tokens/erc1155/erc1155_provider.go @@ -96,12 +96,12 @@ func (p *ERC1155Provider) GetDockerServiceDefinitions(tokenIdx int) []*docker.Se } } - env := map[string]interface{}{ + env := p.stack.ConcatenateEnvironmentVars(map[string]interface{}{ "ETHCONNECT_URL": p.blockchainProvider.GetConnectorURL(member), "ETHCONNECT_TOPIC": connectorName, "AUTO_INIT": "false", "CONTRACT_ADDRESS": contractAddress, - } + }) serviceDefinitions = append(serviceDefinitions, &docker.ServiceDefinition{ ServiceName: connectorName, Service: &docker.Service{ diff --git a/internal/tokens/erc20erc721/erc20_erc721_provider.go b/internal/tokens/erc20erc721/erc20_erc721_provider.go index 61e4e578..000d16da 100644 --- a/internal/tokens/erc20erc721/erc20_erc721_provider.go +++ b/internal/tokens/erc20erc721/erc20_erc721_provider.go @@ -96,11 +96,11 @@ func (p *ERC20ERC721Provider) GetDockerServiceDefinitions(tokenIdx int) []*docke } } - env := map[string]interface{}{ + env := p.stack.ConcatenateEnvironmentVars(map[string]interface{}{ "ETHCONNECT_URL": p.blockchainProvider.GetConnectorURL(member), "ETHCONNECT_TOPIC": connectorName, "AUTO_INIT": "false", - } + }) if !p.stack.DisableTokenFactories && factoryAddress != "" { env["FACTORY_CONTRACT_ADDRESS"] = factoryAddress diff --git a/pkg/types/options.go b/pkg/types/options.go index ed9cdf60..95611623 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -65,6 +65,7 @@ type InitOptions struct { ChaincodeName string CustomPinSupport bool RemoteNodeDeploy bool + EnvironmentVars map[string]string } const IPFSMode = "ipfs_mode" diff --git a/pkg/types/stack.go b/pkg/types/stack.go index 89c5c777..e96add77 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -25,35 +25,36 @@ import ( ) type Stack struct { - Name string `json:"name,omitempty"` - Members []*Organization `json:"members,omitempty"` - SwarmKey string `json:"swarmKey,omitempty"` - ExposedBlockchainPort int `json:"exposedBlockchainPort,omitempty"` - Database fftypes.FFEnum `json:"database"` - BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` - BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` - BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` - TokenProviders []fftypes.FFEnum `json:"tokenProviders"` - VersionManifest *VersionManifest `json:"versionManifest,omitempty"` - PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` - SandboxEnabled bool `json:"sandboxEnabled,omitempty"` - MultipartyEnabled bool `json:"multiparty"` - ExposedPrometheusPort int `json:"exposedPrometheusPort,omitempty"` - ContractAddress string `json:"contractAddress,omitempty"` - ChainIDPtr *int64 `json:"chainID,omitempty"` - RemoteNodeURL string `json:"remoteNodeURL,omitempty"` - DisableTokenFactories bool `json:"disableTokenFactories,omitempty"` - RequestTimeout int `json:"requestTimeout,omitempty"` - IPFSMode fftypes.FFEnum `json:"ipfsMode"` - RemoteFabricNetwork bool `json:"remoteFabricNetwork,omitempty"` - ChannelName string `json:"channelName,omitempty"` - ChaincodeName string `json:"chaincodeName,omitempty"` - CustomPinSupport bool `json:"customPinSupport,omitempty"` - RemoteNodeDeploy bool `json:"remoteNodeDeploy,omitempty"` - InitDir string `json:"-"` - RuntimeDir string `json:"-"` - StackDir string `json:"-"` - State *StackState `json:"-"` + Name string `json:"name,omitempty"` + Members []*Organization `json:"members,omitempty"` + SwarmKey string `json:"swarmKey,omitempty"` + ExposedBlockchainPort int `json:"exposedBlockchainPort,omitempty"` + Database fftypes.FFEnum `json:"database"` + BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` + BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` + BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` + TokenProviders []fftypes.FFEnum `json:"tokenProviders"` + VersionManifest *VersionManifest `json:"versionManifest,omitempty"` + PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` + SandboxEnabled bool `json:"sandboxEnabled,omitempty"` + MultipartyEnabled bool `json:"multiparty"` + ExposedPrometheusPort int `json:"exposedPrometheusPort,omitempty"` + ContractAddress string `json:"contractAddress,omitempty"` + ChainIDPtr *int64 `json:"chainID,omitempty"` + RemoteNodeURL string `json:"remoteNodeURL,omitempty"` + DisableTokenFactories bool `json:"disableTokenFactories,omitempty"` + RequestTimeout int `json:"requestTimeout,omitempty"` + IPFSMode fftypes.FFEnum `json:"ipfsMode"` + RemoteFabricNetwork bool `json:"remoteFabricNetwork,omitempty"` + ChannelName string `json:"channelName,omitempty"` + ChaincodeName string `json:"chaincodeName,omitempty"` + CustomPinSupport bool `json:"customPinSupport,omitempty"` + RemoteNodeDeploy bool `json:"remoteNodeDeploy,omitempty"` + EnvironmentVars map[string]interface{} `json:"environmentVars"` + InitDir string `json:"-"` + RuntimeDir string `json:"-"` + StackDir string `json:"-"` + State *StackState `json:"-"` } func (s *Stack) ChainID() int64 { @@ -106,3 +107,14 @@ func (s *Stack) IsOldFileStructure() (bool, error) { return false, nil } } + +func (s *Stack) ConcatenateEnvironmentVars(input map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for k, v := range input { + result[k] = v + } + for k, v := range s.EnvironmentVars { + result[k] = v // Overwrites existing keys from previous map + } + return result +} From 6c6986727761b2a6e34bfdc300f88d29ce014c39 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 28 Jun 2024 06:13:40 +0000 Subject: [PATCH 02/22] Update concatenate environment variable function name Signed-off-by: rodion --- internal/blockchain/fabric/fabric_docker.go | 6 +++--- internal/docker/docker_config.go | 6 +++--- internal/tokens/erc1155/erc1155_provider.go | 2 +- internal/tokens/erc20erc721/erc20_erc721_provider.go | 2 +- pkg/types/stack.go | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/blockchain/fabric/fabric_docker.go b/internal/blockchain/fabric/fabric_docker.go index 78efbd8e..f2d56122 100644 --- a/internal/blockchain/fabric/fabric_docker.go +++ b/internal/blockchain/fabric/fabric_docker.go @@ -31,7 +31,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricCAImageName, ContainerName: fmt.Sprintf("%s_fabric_ca", s.Name), - Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ + Environment: s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "FABRIC_CA_HOME": "/etc/hyperledger/fabric-ca-server", "FABRIC_CA_SERVER_CA_NAME": "fabric_ca", "FABRIC_CA_SERVER_PORT": "7054", @@ -57,7 +57,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricOrdererImageName, ContainerName: fmt.Sprintf("%s_fabric_orderer", s.Name), - Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ + Environment: s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "FABRIC_LOGGING_SPEC": "INFO", "ORDERER_GENERAL_LISTENADDRESS": "0.0.0.0", "ORDERER_GENERAL_LISTENPORT": "7050", @@ -103,7 +103,7 @@ func GenerateDockerServiceDefinitions(s *types.Stack) []*docker.ServiceDefinitio Service: &docker.Service{ Image: FabricPeerImageName, ContainerName: fmt.Sprintf("%s_fabric_peer", s.Name), - Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ + Environment: s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "CORE_VM_ENDPOINT": "unix:///host/var/run/docker.sock", "CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE": fmt.Sprintf("%s_default", s.Name), "FABRIC_LOGGING_SPEC": "INFO", diff --git a/internal/docker/docker_config.go b/internal/docker/docker_config.go index 06aeff96..74f07166 100644 --- a/internal/docker/docker_config.go +++ b/internal/docker/docker_config.go @@ -112,7 +112,7 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Image: constants.PostgresImageName, ContainerName: fmt.Sprintf("%s_postgres_%s", s.Name, member.ID), Ports: []string{fmt.Sprintf("%d:5432", member.ExposedDatabasePort)}, - Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ + Environment: s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "POSTGRES_PASSWORD": "f1refly", "PGDATA": "/var/lib/postgresql/data/pgdata"}), Volumes: []string{fmt.Sprintf("postgres_%s:/var/lib/postgresql/data", member.ID)}, @@ -149,7 +149,7 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { }, } if s.IPFSMode.Equals(types.IPFSModePrivate) { - sharedStorage.Environment = s.ConcatenateEnvironmentVars(map[string]interface{}{ + sharedStorage.Environment = s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "IPFS_SWARM_KEY": s.SwarmKey, "LIBP2P_FORCE_PNET": "1", }, @@ -174,7 +174,7 @@ func CreateDockerCompose(s *types.Stack) *DockerComposeConfig { Image: constants.SandboxImageName, ContainerName: fmt.Sprintf("%s_sandbox_%s", s.Name, member.ID), Ports: []string{fmt.Sprintf("%d:3001", member.ExposedSandboxPort)}, - Environment: s.ConcatenateEnvironmentVars(map[string]interface{}{ + Environment: s.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "FF_ENDPOINT": fmt.Sprintf("http://firefly_core_%d:%d", *member.Index, member.ExposedFireflyPort), }), } diff --git a/internal/tokens/erc1155/erc1155_provider.go b/internal/tokens/erc1155/erc1155_provider.go index 61e3499d..dbc07aea 100644 --- a/internal/tokens/erc1155/erc1155_provider.go +++ b/internal/tokens/erc1155/erc1155_provider.go @@ -96,7 +96,7 @@ func (p *ERC1155Provider) GetDockerServiceDefinitions(tokenIdx int) []*docker.Se } } - env := p.stack.ConcatenateEnvironmentVars(map[string]interface{}{ + env := p.stack.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "ETHCONNECT_URL": p.blockchainProvider.GetConnectorURL(member), "ETHCONNECT_TOPIC": connectorName, "AUTO_INIT": "false", diff --git a/internal/tokens/erc20erc721/erc20_erc721_provider.go b/internal/tokens/erc20erc721/erc20_erc721_provider.go index 000d16da..ea1dfd31 100644 --- a/internal/tokens/erc20erc721/erc20_erc721_provider.go +++ b/internal/tokens/erc20erc721/erc20_erc721_provider.go @@ -96,7 +96,7 @@ func (p *ERC20ERC721Provider) GetDockerServiceDefinitions(tokenIdx int) []*docke } } - env := p.stack.ConcatenateEnvironmentVars(map[string]interface{}{ + env := p.stack.ConcatenateWithProvidedEnvironmentVars(map[string]interface{}{ "ETHCONNECT_URL": p.blockchainProvider.GetConnectorURL(member), "ETHCONNECT_TOPIC": connectorName, "AUTO_INIT": "false", diff --git a/pkg/types/stack.go b/pkg/types/stack.go index e96add77..5d366ddc 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -108,7 +108,7 @@ func (s *Stack) IsOldFileStructure() (bool, error) { } } -func (s *Stack) ConcatenateEnvironmentVars(input map[string]interface{}) map[string]interface{} { +func (s *Stack) ConcatenateWithProvidedEnvironmentVars(input map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) for k, v := range input { result[k] = v From 4bc7744cce5cb91c4cfecbc07ff692f205571a03 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 31 May 2024 10:30:14 +0000 Subject: [PATCH 03/22] Add quorum blockchain node provider Signed-off-by: rodion --- .../ethereum/private_transaction_manager.go | 156 ++++++++ internal/blockchain/ethereum/quorum/client.go | 93 +++++ .../blockchain/ethereum/quorum/genesis.go | 114 ++++++ .../ethereum/quorum/genesis_test.go | 169 ++++++++ .../ethereum/quorum/geth_provider.go | 360 ++++++++++++++++++ .../ethereum/quorum/geth_provider_test.go | 323 ++++++++++++++++ internal/docker/docker_config.go | 1 + internal/stacks/stack_manager.go | 13 +- pkg/types/options.go | 1 + 9 files changed, 1228 insertions(+), 2 deletions(-) create mode 100644 internal/blockchain/ethereum/private_transaction_manager.go create mode 100644 internal/blockchain/ethereum/quorum/client.go create mode 100644 internal/blockchain/ethereum/quorum/genesis.go create mode 100644 internal/blockchain/ethereum/quorum/genesis_test.go create mode 100644 internal/blockchain/ethereum/quorum/geth_provider.go create mode 100644 internal/blockchain/ethereum/quorum/geth_provider_test.go diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go new file mode 100644 index 00000000..5737a775 --- /dev/null +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -0,0 +1,156 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/hyperledger/firefly-cli/internal/docker" +) + +var entrypoint = "docker-entrypoint.sh" +var tmpath = "/qdata/tm" + +func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name, password string) (string, error) { + // generates both .pub and .key files used by Tessera + var filename string + if prefix != "" { + filename = fmt.Sprintf("%v_%s", prefix, name) + } else { + filename = name + } + if err := os.MkdirAll(outputDirectory, 0755); err != nil { + return "", err + } + fmt.Println("generating tessera keys") + err := docker.RunDockerCommand(ctx, outputDirectory, "run", "--rm", "-v", fmt.Sprintf("%s:/keystore", outputDirectory), image, "-keygen", "-filename", fmt.Sprintf("/keystore/%s", filename)) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%s", outputDirectory, filename), nil +} + +func CopyTesseraKeysToVolume(ctx context.Context, tesseraKeyPath, volumeName string) error { + if err := docker.MkdirInVolume(ctx, volumeName, tmpath); err != nil { + return err + } + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraKeyPath, "tm.pub"), tmpath); err != nil { + return err + } + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraKeyPath, "tm.key"), tmpath); err != nil { + return err + } + return nil +} + +func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, memberCount string) error { + // only tessera v09 onwards is supported + var sb strings.Builder + memberCountInt, _ := strconv.Atoi(memberCount) + for i := 0; i < memberCountInt; i++ { + sb.WriteString(fmt.Sprintf("{\"url\":\"http://member%dtessera:9000\"},", i)) // construct peer list + } + peerList := strings.TrimSuffix(sb.String(), ",") + content := fmt.Sprintf(`export JAVA_OPTS="-Xms128M -Xmx128M" +DDIR=/data/qdata/dd +mkdir -p ${DDIR} +[ -f ${DDIR}/dd/tm.ipc ] && rm -f ${DDIR}/dd/tm.ipc +cat < ${DDIR}/tessera-config-09.json + { + "useWhiteList": false, + "jdbc": { + "username": "sa", + "password": "", + "url": "jdbc:h2:./${DDIR}/db;TRACE_LEVEL_SYSTEM_OUT=0", + "autoCreateTables": true + }, + "serverConfigs":[ + { + "app":"ThirdParty", + "enabled": true, + "serverAddress": "http://$(hostname -i):9080", + "communicationType" : "REST" + }, + { + "app":"Q2T", + "enabled": true, + "serverAddress": "unix:${DDIR}/tm.ipc", + "sslConfig": { + "tls": "OFF" + }, + "communicationType" : "REST" + }, + { + "app":"P2P", + "enabled": true, + "serverAddress": "http://$(hostname -i):9000", + "sslConfig": { + "tls": "OFF" + }, + "communicationType" : "REST" + } + ], + "peer": [ + %s + ], + "keys": { + "passwords": [], + "keyData": [ + { + "privateKeyPath": "${DDIR}/keystore/tm.key", + "publicKeyPath": "${DDIR}/keystore/tm.pub" + } + ] + }, + "alwaysSendTo": [], + "bootstrapNode": false, + "features": { + "enableRemoteKeyValidation": false, + "enablePrivacyEnhancements": true + } + } +EOF +/tessera/bin/tessera -configfile ${DDIR}/tessera-config-09.json +`, peerList) + filename := filepath.Join(outputDirectory, entrypoint) + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content) + if err != nil { + return err + } + CopyTesseraEntrypointToVolume(ctx, outputDirectory, volumeName) + return nil +} + +func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirectory, volumeName string) error { + if err := docker.MkdirInVolume(ctx, volumeName, tmpath); err != nil { + return err + } + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraEntrypointDirectory, entrypoint), tmpath); err != nil { + return err + } + return nil +} diff --git a/internal/blockchain/ethereum/quorum/client.go b/internal/blockchain/ethereum/quorum/client.go new file mode 100644 index 00000000..e4c8c761 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/client.go @@ -0,0 +1,93 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package quorum + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type GethClient struct { + rpcURL string +} + +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` +} + +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Error *JSONRPCError `json:"error,omitempty"` + Result interface{} `json:"result,omitempty"` +} + +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func NewGethClient(rpcURL string) *GethClient { + return &GethClient{ + rpcURL: rpcURL, + } +} + +func (g *GethClient) UnlockAccount(address string, password string) error { + requestBody, err := json.Marshal(&JSONRPCRequest{ + JSONRPC: "2.0", + ID: 0, + Method: "personal_unlockAccount", + Params: []interface{}{address, password, 0}, + }) + if err != nil { + return err + } + req, err := http.NewRequest("POST", g.rpcURL, bytes.NewBuffer(requestBody)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("%s [%d] %s", req.URL, resp.StatusCode, responseBody) + } + var rpcResponse *JSONRPCResponse + err = json.Unmarshal(responseBody, &rpcResponse) + if err != nil { + return err + } + if rpcResponse.Error != nil { + return fmt.Errorf(rpcResponse.Error.Message) + } + return nil +} diff --git a/internal/blockchain/ethereum/quorum/genesis.go b/internal/blockchain/ethereum/quorum/genesis.go new file mode 100644 index 00000000..46ef6bb7 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/genesis.go @@ -0,0 +1,114 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package quorum + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +type Genesis struct { + Config *GenesisConfig `json:"config"` + Nonce string `json:"nonce"` + Timestamp string `json:"timestamp"` + ExtraData string `json:"extraData"` + GasLimit string `json:"gasLimit"` + Difficulty string `json:"difficulty"` + MixHash string `json:"mixHash"` + Coinbase string `json:"coinbase"` + Alloc map[string]*Alloc `json:"alloc"` + Number string `json:"number"` + GasUsed string `json:"gasUsed"` + ParentHash string `json:"parentHash"` +} + +type GenesisConfig struct { + ChainID int64 `json:"chainId"` + HomesteadBlock int `json:"homesteadBlock"` + Eip150Block int `json:"eip150Block"` + Eip150Hash string `json:"eip150Hash"` + Eip155Block int `json:"eip155Block"` + Eip158Block int `json:"eip158Block"` + ByzantiumBlock int `json:"byzantiumBlock"` + ConstantinopleBlock int `json:"constantinopleBlock"` + PetersburgBlock int `json:"petersburgBlock"` + IstanbulBlock int `json:"istanbulBlock"` + Clique *CliqueConfig `json:"clique"` +} + +type CliqueConfig struct { + Period int `json:"period"` + Epoch int `json:"epoch"` +} + +type Alloc struct { + Balance string `json:"balance"` +} + +func CreateGenesis(addresses []string, blockPeriod int, chainID int64) *Genesis { + if blockPeriod == -1 { + blockPeriod = 0 + } + extraData := "0x0000000000000000000000000000000000000000000000000000000000000000" + alloc := make(map[string]*Alloc) + for _, address := range addresses { + alloc[address] = &Alloc{ + Balance: "0x200000000000000000000000000000000000000000000000000000000000000", + } + extraData += address + } + extraData = strings.ReplaceAll(fmt.Sprintf("%-236s", extraData), " ", "0") + + return &Genesis{ + Config: &GenesisConfig{ + ChainID: chainID, + HomesteadBlock: 0, + Eip150Block: 0, + Eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + Eip155Block: 0, + Eip158Block: 0, + ByzantiumBlock: 0, + ConstantinopleBlock: 0, + IstanbulBlock: 0, + Clique: &CliqueConfig{ + Period: blockPeriod, + Epoch: 30000, + }, + }, + Nonce: "0x0", + Timestamp: "0x60edb1c7", + ExtraData: extraData, + GasLimit: "0xffffff", + Difficulty: "0x1", + MixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", + Coinbase: "0x0000000000000000000000000000000000000000", + Alloc: alloc, + Number: "0x0", + GasUsed: "0x0", + ParentHash: "0x0000000000000000000000000000000000000000000000000000000000000000", + } +} + +func (g *Genesis) WriteGenesisJSON(filename string) error { + genesisJSONBytes, _ := json.MarshalIndent(g, "", " ") + if err := os.WriteFile(filename, genesisJSONBytes, 0755); err != nil { + return err + } + return nil +} diff --git a/internal/blockchain/ethereum/quorum/genesis_test.go b/internal/blockchain/ethereum/quorum/genesis_test.go new file mode 100644 index 00000000..7e47856e --- /dev/null +++ b/internal/blockchain/ethereum/quorum/genesis_test.go @@ -0,0 +1,169 @@ +package quorum + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateGenesis(t *testing.T) { + testCases := []struct { + Name string + addresses []string + blockPeriod int + chainID int64 + }{ + { + Name: "testcase1", + addresses: []string{"0xAddress20", "0xAddress27"}, + blockPeriod: 28, + chainID: int64(21), + }, + { + Name: "testcase2", + addresses: []string{"0xAddress36", "0xAddress45"}, + blockPeriod: 26, + chainID: int64(98), + }, + { + Name: "testcase3", + addresses: []string{"0xAddress19", "0xAddress38", "0xAddress64", "0xAddress74"}, + blockPeriod: 40, + chainID: int64(93), + }, + { + Name: "testcase4", + addresses: []string{"0xAddress96", "0xAddress25", "0xAddress49", "0xAddress24", "0xAddress37", "0xAddress12"}, + blockPeriod: 12, + chainID: int64(5000), + }, + { + Name: "testcase5", + addresses: []string{"0xAddress62536", "0xAddress3261", "0xAddress82721"}, + blockPeriod: 14, + chainID: int64(900000), + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + genesis := CreateGenesis(tc.addresses, tc.blockPeriod, tc.chainID) + extraData := "0x0000000000000000000000000000000000000000000000000000000000000000" + alloc := make(map[string]*Alloc) + for _, address := range tc.addresses { + alloc[address] = &Alloc{ + "0x200000000000000000000000000000000000000000000000000000000000000", + } + extraData += address + } + extraData = strings.ReplaceAll(fmt.Sprintf("%-236s", extraData), " ", "0") + expectedGenesis := &Genesis{ + Config: &GenesisConfig{ + ChainID: tc.chainID, + HomesteadBlock: 0, + Eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + Eip155Block: 0, + Eip158Block: 0, + ByzantiumBlock: 0, + ConstantinopleBlock: 0, + Clique: &CliqueConfig{ + Period: tc.blockPeriod, + Epoch: 30000, + }, + }, + Nonce: "0x0", + Timestamp: "0x60edb1c7", + ExtraData: extraData, + GasLimit: "0xffffff", + Difficulty: "0x1", + MixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", + Coinbase: "0x0000000000000000000000000000000000000000", + Alloc: alloc, + Number: "0x0", + GasUsed: "0x0", + ParentHash: "0x0000000000000000000000000000000000000000000000000000000000000000", + } + // Assert that the generated Genesis is equal to the expected Genesis + assert.Equal(t, expectedGenesis, genesis, "Generated Genesis does not match the expected Genesis") + + }) + } +} + +func TestWriteGenesisJSON(t *testing.T) { + filepath := t.TempDir() + + testCases := []struct { + Name string + SampleGenesis Genesis + filename string + }{ + { + Name: "TestCase1", + SampleGenesis: Genesis{ + Config: &GenesisConfig{ + ChainID: int64(456), + Eip155Block: 0, + Eip158Block: 0, + ByzantiumBlock: 0, + ConstantinopleBlock: 0, + IstanbulBlock: 0, + Clique: &CliqueConfig{ + Period: 20, + Epoch: 2000, + }, + }, + }, + filename: filepath + "/genesis1_output.json", + }, + { + Name: "TestCase2", + SampleGenesis: Genesis{ + Config: &GenesisConfig{ + ChainID: int64(338), + ConstantinopleBlock: 0, + Eip155Block: 0, + Eip158Block: 0, + ByzantiumBlock: 0, + IstanbulBlock: 0, + Clique: &CliqueConfig{ + Period: 40, + Epoch: 4000, + }, + }, + }, + filename: filepath + "/genesis2_output.json", + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + err := tc.SampleGenesis.WriteGenesisJSON(tc.filename) + if err != nil { + t.Log("unable to write Genesis JSON", err) + } + // Assert that there is no error + assert.NoError(t, err) + + writtenJSONBytes, err := os.ReadFile(tc.filename) + if err != nil { + t.Log("Unable to write JSON Bytes", err) + } + assert.NoError(t, err) + var writtenGenesis Genesis + + err = json.Unmarshal(writtenJSONBytes, &writtenGenesis) + if err != nil { + t.Log("unable to unmarshal JSON", err) + } + assert.NoError(t, err) + + // Assert that the written Genesis matches the original Genesis + assert.Equal(t, tc.SampleGenesis, writtenGenesis) + }) + + } + +} diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go new file mode 100644 index 00000000..b70d6574 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -0,0 +1,360 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package quorum + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum" + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector" + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector/ethconnect" + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector/evmconnect" + "github.com/hyperledger/firefly-cli/internal/constants" + "github.com/hyperledger/firefly-cli/internal/docker" + "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/pkg/types" +) + +var gethImage = "quorumengineering/quorum:24.4" +var tesseraImage = "quorumengineering/tessera:24.4" + +// TODO: Probably randomize this and make it different per member? +var keyPassword = "correcthorsebatterystaple" + +type GethProvider struct { + ctx context.Context + stack *types.Stack + connector connector.Connector +} + +func NewGethProvider(ctx context.Context, stack *types.Stack) *GethProvider { + var connector connector.Connector + switch stack.BlockchainConnector { + case types.BlockchainConnectorEthconnect: + connector = ethconnect.NewEthconnect(ctx) + case types.BlockchainConnectorEvmconnect: + connector = evmconnect.NewEvmconnect(ctx) + } + + return &GethProvider{ + ctx: ctx, + stack: stack, + connector: connector, + } +} + +func (p *GethProvider) WriteConfig(options *types.InitOptions) error { + initDir := filepath.Join(constants.StacksDir, p.stack.Name, "init") + for i, member := range p.stack.Members { + // Generate the connector config for each member + connectorConfigPath := filepath.Join(initDir, "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) + if err := p.connector.GenerateConfig(p.stack, member, "geth_0").WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { + return nil + } + } + + // Create genesis.json + addresses := make([]string, len(p.stack.Members)) + for i, member := range p.stack.Members { + address := member.Account.(*ethereum.Account).Address + // Drop the 0x on the front of the address here because that's what geth is expecting in the genesis.json + addresses[i] = address[2:] + } + genesis := CreateGenesis(addresses, options.BlockPeriod, p.stack.ChainID()) + if err := genesis.WriteGenesisJSON(filepath.Join(initDir, "blockchain", "genesis.json")); err != nil { + return err + } + + return nil +} + +func (p *GethProvider) FirstTimeSetup() error { + gethVolumeName := fmt.Sprintf("%s_geth_0", p.stack.Name) + tesseraVolumeName := fmt.Sprintf("%s_tessera_0", p.stack.Name) + blockchainDir := path.Join(p.stack.RuntimeDir, "blockchain") + tesseraDir := path.Join(p.stack.RuntimeDir, "tessera") + tesseraDirWithinContainer := "/qdata/dd" + contractsDir := path.Join(p.stack.RuntimeDir, "contracts") + + if err := p.connector.FirstTimeSetup(p.stack); err != nil { + return err + } + + if err := os.MkdirAll(contractsDir, 0755); err != nil { + return err + } + + for i := range p.stack.Members { + // Copy connector config to each member's volume + connectorConfigPath := filepath.Join(p.stack.StackDir, "runtime", "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) + connectorConfigVolumeName := fmt.Sprintf("%s_%s_config_%v", p.stack.Name, p.connector.Name(), i) + if err := docker.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { + return err + } + } + + // Copy the wallet files of all members to the blockchain volume + keystoreDirectory := filepath.Join(blockchainDir, "keystore") + if err := docker.CopyFileToVolume(p.ctx, gethVolumeName, keystoreDirectory, "/"); err != nil { + return err + } + + // Copy member specific tessera key files and docker entrypoint files to each of the tessera volume + for i := range p.stack.Members { + if err := docker.MkdirInVolume(p.ctx, tesseraVolumeName, tesseraDirWithinContainer); err != nil { + return err + } + tmDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeName, tmDirectory, tesseraDirWithinContainer); err != nil { + return err + } + entrypointFile := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "docker-entrypoint.sh") + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeName, entrypointFile, tesseraDirWithinContainer); err != nil { + return err + } + } + + // Copy the genesis block information + if err := docker.CopyFileToVolume(p.ctx, gethVolumeName, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { + return err + } + + // Initialize the genesis block + if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", gethVolumeName), gethImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { + return err + } + + return nil +} + +func (p *GethProvider) PreStart() error { + return nil +} + +func (p *GethProvider) PostStart(firstTimeSetup bool) error { + l := log.LoggerFromContext(p.ctx) + // Unlock accounts + for _, account := range p.stack.State.Accounts { + address := account.(*ethereum.Account).Address + l.Info(fmt.Sprintf("unlocking account %s", address)) + if err := p.unlockAccount(address, keyPassword); err != nil { + return err + } + } + + return nil +} + +func (p *GethProvider) unlockAccount(address, password string) error { + l := log.LoggerFromContext(p.ctx) + verbose := log.VerbosityFromContext(p.ctx) + gethClient := NewGethClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort)) + retries := 10 + for { + if err := gethClient.UnlockAccount(address, password); err != nil { + if verbose { + l.Debug(err.Error()) + } + if retries == 0 { + return fmt.Errorf("unable to unlock account %s", address) + } + time.Sleep(time.Second * 1) + retries-- + } else { + break + } + } + return nil +} + +func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, error) { + contract, err := ethereum.ReadFireFlyContract(p.ctx, p.stack) + if err != nil { + return nil, err + } + return p.connector.DeployContract(contract, "FireFly", p.stack.Members[0], nil) +} + +func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { + gethCommand := fmt.Sprintf(`--datadir /data --syncmode 'full' --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port 8545 --http.vhosts "*" --http.api 'admin,personal,eth,net,web3,txpool,miner,clique,debug' --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --nodiscover --verbosity 4 --miner.gaslimit 16777215`, p.stack.ChainID()) + + serviceDefinitions := make([]*docker.ServiceDefinition, 2) + serviceDefinitions[0] = &docker.ServiceDefinition{ + ServiceName: "geth_0", + Service: &docker.Service{ + Image: gethImage, + ContainerName: fmt.Sprintf("%s_geth_0", p.stack.Name), + Command: gethCommand, + Volumes: []string{"geth_0:/data"}, + Logging: docker.StandardLogOptions, + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort)}, + Environment: p.stack.EnvironmentVars, + DependsOn: map[string]map[string]string{"tessera_0": {"condition": "service_started"}}, + }, + VolumeNames: []string{"geth_0"}, + } + serviceDefinitions[1] = &docker.ServiceDefinition{ + ServiceName: "tessera_0", + Service: &docker.Service{ + Image: tesseraImage, + ContainerName: fmt.Sprintf("member%stessera", "0"), + Volumes: []string{"tessera_0:/data"}, + Logging: docker.StandardLogOptions, + Environment: p.stack.EnvironmentVars, + EntryPoint: []string{"/bin/sh", "-c", "/data/qdata/dd/docker-entrypoint.sh"}, + Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, + }, + VolumeNames: []string{"tessera_0"}, + } + serviceDefinitions = append(serviceDefinitions, p.connector.GetServiceDefinitions(p.stack, map[string]string{"geth_0": "service_started"})...) + return serviceDefinitions +} + +func (p *GethProvider) GetBlockchainPluginConfig(stack *types.Stack, m *types.Organization) (blockchainConfig *types.BlockchainConfig) { + var connectorURL string + if m.External { + connectorURL = p.GetConnectorExternalURL(m) + } else { + connectorURL = p.GetConnectorURL(m) + } + + blockchainConfig = &types.BlockchainConfig{ + Type: "ethereum", + Ethereum: &types.EthereumConfig{ + Ethconnect: &types.EthconnectConfig{ + URL: connectorURL, + Topic: m.ID, + }, + }, + } + return +} + +func (p *GethProvider) GetOrgConfig(stack *types.Stack, m *types.Organization) (orgConfig *types.OrgConfig) { + account := m.Account.(*ethereum.Account) + orgConfig = &types.OrgConfig{ + Name: m.OrgName, + Key: account.Address, + } + return +} + +func (p *GethProvider) Reset() error { + return nil +} + +func (p *GethProvider) GetContracts(filename string, extraArgs []string) ([]string, error) { + contracts, err := ethereum.ReadContractJSON(filename) + if err != nil { + return []string{}, err + } + contractNames := make([]string, len(contracts.Contracts)) + i := 0 + for contractName := range contracts.Contracts { + contractNames[i] = contractName + i++ + } + return contractNames, err +} + +func (p *GethProvider) DeployContract(filename, contractName, instanceName string, member *types.Organization, extraArgs []string) (*types.ContractDeploymentResult, error) { + contracts, err := ethereum.ReadContractJSON(filename) + if err != nil { + return nil, err + } + return p.connector.DeployContract(contracts.Contracts[contractName], instanceName, member, extraArgs) +} + +func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { + l := log.LoggerFromContext(p.ctx) + gethVolumeName := fmt.Sprintf("%s_geth_0", p.stack.Name) + tesseraVolumeName := fmt.Sprintf("%s_tessera_0", p.stack.Name) + var directory string + stackHasRunBefore, err := p.stack.HasRunBefore() + if err != nil { + return nil, err + } + if stackHasRunBefore { + directory = p.stack.RuntimeDir + } else { + directory = p.stack.InitDir + } + + prefix := strconv.FormatInt(time.Now().UnixNano(), 10) + outputDirectory := filepath.Join(directory, "blockchain", "keystore") + keyPair, walletFilePath, err := ethereum.CreateWalletFile(outputDirectory, prefix, keyPassword) + if err != nil { + return nil, err + } + tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", args[2]), "keystore") + tesseraKeysPath, err := ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + if err != nil { + return nil, err + } + l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) + tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", args[2])) + if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, args[3]); err != nil { + return nil, err + } + + if stackHasRunBefore { + if err := ethereum.CopyWalletFileToVolume(p.ctx, walletFilePath, gethVolumeName); err != nil { + return nil, err + } + if err := p.unlockAccount(keyPair.Address.String(), keyPassword); err != nil { + return nil, err + } + if err := ethereum.CopyTesseraKeysToVolume(p.ctx, tesseraKeysOutputDirectory, tesseraVolumeName); err != nil { + return nil, err + } + if err := ethereum.CopyTesseraEntrypointToVolume(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName); err != nil { + return nil, err + } + } + + return ðereum.Account{ + Address: keyPair.Address.String(), + PrivateKey: hex.EncodeToString(keyPair.PrivateKeyBytes()), + }, nil +} + +func (p *GethProvider) ParseAccount(account interface{}) interface{} { + accountMap := account.(map[string]interface{}) + return ðereum.Account{ + Address: accountMap["address"].(string), + PrivateKey: accountMap["privateKey"].(string), + } +} + +func (p *GethProvider) GetConnectorName() string { + return p.connector.Name() +} + +func (p *GethProvider) GetConnectorURL(org *types.Organization) string { + return fmt.Sprintf("http://%s_%s:%v", p.connector.Name(), org.ID, p.connector.Port()) +} + +func (p *GethProvider) GetConnectorExternalURL(org *types.Organization) string { + return fmt.Sprintf("http://127.0.0.1:%v", org.ExposedConnectorPort) +} diff --git a/internal/blockchain/ethereum/quorum/geth_provider_test.go b/internal/blockchain/ethereum/quorum/geth_provider_test.go new file mode 100644 index 00000000..954791d7 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/geth_provider_test.go @@ -0,0 +1,323 @@ +package quorum + +import ( + "context" + "encoding/hex" + "os" + "path/filepath" + + "testing" + + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/stretchr/testify/assert" +) + +func TestNewGethProvider(t *testing.T) { + var ctx context.Context + + testcases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + }{ + { + Name: "testcase1", + Ctx: ctx, + Stack: &types.Stack{ + Name: "TestGethWithEvmConnect", + Members: []*types.Organization{ + { + OrgName: "Org1", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + }, + }, + { + OrgName: "Org2", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1234567890abcdef012345670000000000000000", + PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", + }, + }, + }, + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + }, + }, + { + Name: "testcase2", + Ctx: ctx, + Stack: &types.Stack{ + Name: "TestGethWithEthConnect", + Members: []*types.Organization{ + { + OrgName: "Org55", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1f2a000000000000000000000000000000000000", + PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", + }, + }, + }, + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + }, + }, + } + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + gethProvider := NewGethProvider(tc.Ctx, tc.Stack) + assert.NotNil(t, gethProvider) + }) + } +} + +func TestParseAccount(t *testing.T) { + testcases := []struct { + Name string + Address map[string]interface{} + ExpectedAccount *ethereum.Account + }{ + { + Name: "Account 1", + Address: map[string]interface{}{ + "address": "0x1234567890abcdef0123456789abcdef6789abcd", + "privateKey": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + }, + ExpectedAccount: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + }, + }, + { + Name: "Account 2", + Address: map[string]interface{}{ + "address": "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", + "privateKey": "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + }, + ExpectedAccount: ðereum.Account{ + Address: "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", + PrivateKey: "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + }, + }, + } + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + gethProvider := &GethProvider{} + result := gethProvider.ParseAccount(tc.Address) + + _, ok := result.(*ethereum.Account) + if !ok { + t.Errorf("Expected result to be of type *ethereum.Account, but got %T", result) + } + assert.Equal(t, tc.ExpectedAccount, result, "Generated result unmatched") + }) + } +} + +func TestGetOrgConfig(t *testing.T) { + testCases := []struct { + Name string + Org *types.Organization + Stack *types.Stack + OrgConfig *types.OrgConfig + }{ + { + Name: "Testcase1", + Stack: &types.Stack{ + Name: "Org-1", + }, + Org: &types.Organization{ + OrgName: "Org-1", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + }, + }, + OrgConfig: &types.OrgConfig{ + Name: "Org-1", + Key: "0x1234567890abcdef0123456789abcdef6789abcd", + }, + }, + { + Name: "Testcase2", + Stack: &types.Stack{ + Name: "Org-2", + }, + Org: &types.Organization{ + OrgName: "Org-2", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1f2a000000000000000000000000000000000000", + PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", + }, + }, + OrgConfig: &types.OrgConfig{ + Name: "Org-2", + Key: "0x1f2a000000000000000000000000000000000000", + }, + }, + { + Name: "Testcase3", + Stack: &types.Stack{ + Name: "Org-3", + }, + Org: &types.Organization{ + OrgName: "Org-3", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0xabcdeffedcba9876543210abcdeffedc00000000", + PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", + }, + }, + OrgConfig: &types.OrgConfig{ + Name: "Org-3", + Key: "0xabcdeffedcba9876543210abcdeffedc00000000", + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + p := &GethProvider{} + + Orgconfig := p.GetOrgConfig(tc.Stack, tc.Org) + assert.NotNil(t, Orgconfig) + assert.Equal(t, tc.OrgConfig, Orgconfig) + }) + } +} + +func TestGetContracts(t *testing.T) { + FilePath := t.TempDir() + testContractFile := filepath.Join(FilePath, "/test_contracts.json") + // Sample contract JSON content for testing + const testContractJSON = `{ + "contracts": { + "Contract1": { + "name": "sample_1", + "abi": "sample_abi_1", + "bin": "sample_bin_1" + }, + "Contract2": { + "name": "sample_2", + "abi": "sample_abi_2", + "bin": "sample_bin_2" + } + } + }` + p := &GethProvider{} + + err := os.WriteFile(testContractFile, []byte(testContractJSON), 0755) + if err != nil { + t.Log("unable to write file:", err) + } + contracts, err := p.GetContracts(testContractFile, nil) + if err != nil { + t.Log("unable to get contract", err) + } + // Assert that the returned contracts match the expected contract names + expectedContracts := []string{"Contract1", "Contract2"} + assert.ElementsMatch(t, contracts, expectedContracts) +} +func TestGetConnectorExternal(t *testing.T) { + testcase := []struct { + Name string + Org *types.Organization + ExpectedPort string + }{ + { + Name: "testcase1", + Org: &types.Organization{ + OrgName: "Org-1", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0x1f2a000000000000000000000000000000000000", + PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", + }, + ExposedConnectorPort: 8584, + }, + ExpectedPort: "http://127.0.0.1:8584", + }, + { + Name: "testcase2", + Org: &types.Organization{ + OrgName: "Org-2", + NodeName: "geth", + Account: ðereum.Account{ + Address: "0xabcdeffedcba9876543210abcdeffedc00000000", + PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", + }, + ExposedConnectorPort: 8000, + }, + ExpectedPort: "http://127.0.0.1:8000", + }, + } + for _, tc := range testcase { + p := &GethProvider{} + result := p.GetConnectorExternalURL(tc.Org) + assert.Equal(t, tc.ExpectedPort, result) + } +} + +func TestCreateAccount(t *testing.T) { + testcases := []struct { + Name string + Stack *types.Stack + Args []string + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_geth", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + }, + Args: []string{}, + }, + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-2_geth", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + }, + Args: []string{}, + }, + } + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + p := &GethProvider{ + stack: tc.Stack, + } + Account, err := p.CreateAccount(tc.Args) + if err != nil { + t.Log("unable to create account", err) + } + //validate properties of account + assert.NotNil(t, Account) + account, ok := Account.(*ethereum.Account) + assert.True(t, ok, "unexpected Type for account") + + //check if Ethereum Addresss is valid + assert.NotEmpty(t, account.Address) + // Check if the private key is a non-empty hex string + assert.NotEmpty(t, account.PrivateKey) + _, err = hex.DecodeString(account.PrivateKey) + assert.NoError(t, err, "invalid private key format") + }) + } +} diff --git a/internal/docker/docker_config.go b/internal/docker/docker_config.go index 74f07166..897067a8 100644 --- a/internal/docker/docker_config.go +++ b/internal/docker/docker_config.go @@ -60,6 +60,7 @@ type Service struct { EntryPoint []string `yaml:"entrypoint,omitempty"` EnvFile string `yaml:"env_file,omitempty"` Expose []int `yaml:"expose,omitempty"` + Deploy map[string]interface{} `yaml:"deploy,omitempty"` } type DockerComposeConfig struct { diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index b1075b53..cfa60902 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -25,6 +25,7 @@ import ( "os/exec" "path" "path/filepath" + "strconv" "strings" "sync" "syscall" @@ -34,6 +35,7 @@ import ( "github.com/hyperledger/firefly-cli/internal/blockchain" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/besu" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/geth" + "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/quorum" ethremoterpc "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/remoterpc" "github.com/hyperledger/firefly-cli/internal/blockchain/fabric" tezosremoterpc "github.com/hyperledger/firefly-cli/internal/blockchain/tezos/remoterpc" @@ -584,7 +586,7 @@ func (s *StackManager) createMember(id string, index int, options *types.InitOpt nextPort++ } - account, err := s.blockchainProvider.CreateAccount([]string{member.OrgName, member.OrgName}) + account, err := s.blockchainProvider.CreateAccount([]string{member.OrgName, member.OrgName, strconv.Itoa(index), strconv.Itoa(options.MemberCount)}) if err != nil { return nil, err } @@ -1289,6 +1291,11 @@ func (s *StackManager) getBlockchainProvider() blockchain.IBlockchainProvider { s.Stack.BlockchainNodeProvider = types.BlockchainNodeProviderGeth } + if s.Stack.BlockchainProvider.Equals(types.BlockchainNodeProviderQuorum) { + s.Stack.BlockchainProvider = types.BlockchainProviderEthereum + s.Stack.BlockchainNodeProvider = types.BlockchainNodeProviderQuorum + } + if s.Stack.BlockchainProvider.Equals(types.BlockchainNodeProviderBesu) { s.Stack.BlockchainProvider = types.BlockchainProviderEthereum s.Stack.BlockchainNodeProvider = types.BlockchainNodeProviderBesu @@ -1297,7 +1304,7 @@ func (s *StackManager) getBlockchainProvider() blockchain.IBlockchainProvider { // Fallbacks for old stacks that don't have a specific blockchain connector set if s.Stack.BlockchainConnector == "" { switch s.Stack.BlockchainProvider { - case types.BlockchainProviderEthereum, types.BlockchainNodeProviderGeth, types.BlockchainNodeProviderBesu: + case types.BlockchainProviderEthereum, types.BlockchainNodeProviderGeth, types.BlockchainNodeProviderQuorum, types.BlockchainNodeProviderBesu: // Ethconnect used to be the only option for ethereum before it was configurable so set it as the fallback s.Stack.BlockchainConnector = types.BlockchainConnectorEthconnect case types.BlockchainProviderFabric: @@ -1315,6 +1322,8 @@ func (s *StackManager) getBlockchainProvider() blockchain.IBlockchainProvider { return geth.NewGethProvider(s.ctx, s.Stack) case types.BlockchainNodeProviderBesu: return besu.NewBesuProvider(s.ctx, s.Stack) + case types.BlockchainNodeProviderQuorum: + return quorum.NewGethProvider(s.ctx, s.Stack) case types.BlockchainNodeProviderRemoteRPC: s.Stack.DisableTokenFactories = true return ethremoterpc.NewRemoteRPCProvider(s.ctx, s.Stack) diff --git a/pkg/types/options.go b/pkg/types/options.go index 95611623..f1579c8f 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -97,6 +97,7 @@ const BlockchainNodeProvider = "blockchain_node_provider" var ( BlockchainNodeProviderGeth = fftypes.FFEnumValue(BlockchainNodeProvider, "geth") + BlockchainNodeProviderQuorum = fftypes.FFEnumValue(BlockchainNodeProvider, "quorum") BlockchainNodeProviderBesu = fftypes.FFEnumValue(BlockchainNodeProvider, "besu") BlockchainNodeProviderRemoteRPC = fftypes.FFEnumValue(BlockchainNodeProvider, "remote-rpc") ) From 2056886eb08924435ac950681ab53705aff2dda3 Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 20 Jun 2024 08:57:35 +0000 Subject: [PATCH 04/22] Start multiple quorum and tessera containers Signed-off-by: rodion --- .../ethereum/quorum/geth_provider.go | 109 ++++++++++-------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index b70d6574..56815ea3 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -69,7 +69,8 @@ func (p *GethProvider) WriteConfig(options *types.InitOptions) error { for i, member := range p.stack.Members { // Generate the connector config for each member connectorConfigPath := filepath.Join(initDir, "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) - if err := p.connector.GenerateConfig(p.stack, member, "geth_0").WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { + // TODO: remove hardcoding to geth_0 once we connect the quorum network together + if err := p.connector.GenerateConfig(p.stack, member, fmt.Sprintf("geth_%d", 0)).WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { return nil } } @@ -90,8 +91,8 @@ func (p *GethProvider) WriteConfig(options *types.InitOptions) error { } func (p *GethProvider) FirstTimeSetup() error { - gethVolumeName := fmt.Sprintf("%s_geth_0", p.stack.Name) - tesseraVolumeName := fmt.Sprintf("%s_tessera_0", p.stack.Name) + gethVolumeName := fmt.Sprintf("%s_geth", p.stack.Name) + tesseraVolumeName := fmt.Sprintf("%s_tessera", p.stack.Name) blockchainDir := path.Join(p.stack.RuntimeDir, "blockchain") tesseraDir := path.Join(p.stack.RuntimeDir, "tessera") tesseraDirWithinContainer := "/qdata/dd" @@ -114,35 +115,39 @@ func (p *GethProvider) FirstTimeSetup() error { } } - // Copy the wallet files of all members to the blockchain volume - keystoreDirectory := filepath.Join(blockchainDir, "keystore") - if err := docker.CopyFileToVolume(p.ctx, gethVolumeName, keystoreDirectory, "/"); err != nil { - return err - } - - // Copy member specific tessera key files and docker entrypoint files to each of the tessera volume for i := range p.stack.Members { - if err := docker.MkdirInVolume(p.ctx, tesseraVolumeName, tesseraDirWithinContainer); err != nil { + gethVolumeNameMember := fmt.Sprintf("%s_%d", gethVolumeName, i) + tesseraVolumeNameMember := fmt.Sprintf("%s_%d", tesseraVolumeName, i) + + // Copy the wallet files of all members to the blockchain volume (TODO) change to only relevant keys + keystoreDirectory := filepath.Join(blockchainDir, "keystore") + if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, keystoreDirectory, "/"); err != nil { + return err + } + + // Copy member specific tessera key files and docker entrypoint files to each of the tessera volume + if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, tesseraDirWithinContainer); err != nil { return err } tmDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeName, tmDirectory, tesseraDirWithinContainer); err != nil { + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmDirectory, tesseraDirWithinContainer); err != nil { return err } entrypointFile := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "docker-entrypoint.sh") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeName, entrypointFile, tesseraDirWithinContainer); err != nil { + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, entrypointFile, tesseraDirWithinContainer); err != nil { return err } - } - // Copy the genesis block information - if err := docker.CopyFileToVolume(p.ctx, gethVolumeName, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { - return err - } + // Copy the genesis block information + if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { + return err + } + + // Initialize the genesis block + if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", gethVolumeNameMember), gethImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { + return err + } - // Initialize the genesis block - if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", gethVolumeName), gethImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { - return err } return nil @@ -198,36 +203,40 @@ func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { gethCommand := fmt.Sprintf(`--datadir /data --syncmode 'full' --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port 8545 --http.vhosts "*" --http.api 'admin,personal,eth,net,web3,txpool,miner,clique,debug' --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --nodiscover --verbosity 4 --miner.gaslimit 16777215`, p.stack.ChainID()) - - serviceDefinitions := make([]*docker.ServiceDefinition, 2) - serviceDefinitions[0] = &docker.ServiceDefinition{ - ServiceName: "geth_0", - Service: &docker.Service{ - Image: gethImage, - ContainerName: fmt.Sprintf("%s_geth_0", p.stack.Name), - Command: gethCommand, - Volumes: []string{"geth_0:/data"}, - Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort)}, - Environment: p.stack.EnvironmentVars, - DependsOn: map[string]map[string]string{"tessera_0": {"condition": "service_started"}}, - }, - VolumeNames: []string{"geth_0"}, - } - serviceDefinitions[1] = &docker.ServiceDefinition{ - ServiceName: "tessera_0", - Service: &docker.Service{ - Image: tesseraImage, - ContainerName: fmt.Sprintf("member%stessera", "0"), - Volumes: []string{"tessera_0:/data"}, - Logging: docker.StandardLogOptions, - Environment: p.stack.EnvironmentVars, - EntryPoint: []string{"/bin/sh", "-c", "/data/qdata/dd/docker-entrypoint.sh"}, - Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, - }, - VolumeNames: []string{"tessera_0"}, + memberCount := len(p.stack.Members) + serviceDefinitions := make([]*docker.ServiceDefinition, 2*memberCount) + connectorDependents := map[string]string{} + for i := 0; i < memberCount; i++ { + serviceDefinitions[i] = &docker.ServiceDefinition{ + ServiceName: fmt.Sprintf("geth_%d", i), + Service: &docker.Service{ + Image: gethImage, + ContainerName: fmt.Sprintf("%s_geth_%d", p.stack.Name, i), + Command: gethCommand, + Volumes: []string{fmt.Sprintf("geth_%d:/data", i)}, + Logging: docker.StandardLogOptions, + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*10))}, // defaults 5100, 5110, 5120, 5130 + Environment: p.stack.EnvironmentVars, + DependsOn: map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}}, + }, + VolumeNames: []string{fmt.Sprintf("geth_%d", i)}, + } + serviceDefinitions[i+memberCount] = &docker.ServiceDefinition{ + ServiceName: fmt.Sprintf("tessera_%d", i), + Service: &docker.Service{ + Image: tesseraImage, + ContainerName: fmt.Sprintf("member%dtessera", i), + Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, + Logging: docker.StandardLogOptions, + Environment: p.stack.EnvironmentVars, + EntryPoint: []string{"/bin/sh", "-c", "/data/qdata/dd/docker-entrypoint.sh"}, + Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, + }, + VolumeNames: []string{fmt.Sprintf("tessera_%d", i)}, + } + connectorDependents[fmt.Sprintf("geth_%d", i)] = "service_started" } - serviceDefinitions = append(serviceDefinitions, p.connector.GetServiceDefinitions(p.stack, map[string]string{"geth_0": "service_started"})...) + serviceDefinitions = append(serviceDefinitions, p.connector.GetServiceDefinitions(p.stack, connectorDependents)...) return serviceDefinitions } From 85013cfb6becbf15ac6dfa2e001abe7caf1b43fe Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 20 Jun 2024 10:16:35 +0000 Subject: [PATCH 05/22] Switch to using entrypoint file for quorum Signed-off-by: rodion --- .../ethereum/private_transaction_manager.go | 79 ++++++++++++++++++- .../ethereum/quorum/geth_provider.go | 27 ++++--- internal/stacks/stack_manager.go | 11 ++- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index 5737a775..968e6b15 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -146,10 +146,85 @@ EOF } func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirectory, volumeName string) error { - if err := docker.MkdirInVolume(ctx, volumeName, tmpath); err != nil { + if err := docker.MkdirInVolume(ctx, volumeName, ""); err != nil { + return err + } + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraEntrypointDirectory, entrypoint), ""); err != nil { + return err + } + return nil +} + +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex string, chainId int) error { + discoveryCmd := "BOOTNODE_CMD=\"\"" + if memberIndex != "0" { + discoveryCmd = `bootnode=$(curl http://geth_0:8545 -s --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") +BOOTNODE_CMD="--bootnodes $bootnode" +BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}` + } + + content := fmt.Sprintf(`#!/bin/sh + +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace + +GOQUORUM_CONS_ALGO=$(echo "${GOQUORUM_CONS_ALGO:-clique}" | tr '[:lower:]') +if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using istanbul for consensus algorithm..." + export CONSENSUS_ARGS="--istanbul.blockperiod 5 --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" + export QUORUM_API="istanbul" +elif [ "qbft" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using qbft for consensus algorithm..." + export CONSENSUS_ARGS="--mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" + export QUORUM_API="istanbul" +elif [ "raft" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using raft for consensus algorithm..." + export CONSENSUS_ARGS="--raft --raftblocktime 300 --raftport 53000" + export QUORUM_API="raft" +elif [ "clique" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using clique for consensus algorithm..." + export CONSENSUS_ARGS="" + export QUORUM_API="clique" +fi + +TESSERA_URL=http://member%stessera:9000/upcheck +ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" + +echo -n "Checking tessera is up ... " +curl --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_URL" +echo "" + +# discovery +%s +echo "bootnode discovery command :: $BOOTNODE_CMD" +IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') + +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port 8545 --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD`, memberIndex, discoveryCmd, chainId) + filename := filepath.Join(outputDirectory, entrypoint) + if err := os.MkdirAll(outputDirectory, 0755); err != nil { + return err + } + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { return err } - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraEntrypointDirectory, entrypoint), tmpath); err != nil { + defer file.Close() + _, err = file.WriteString(content) + if err != nil { + return err + } + CopyQuorumEntrypointToVolume(ctx, outputDirectory, volumeName) + return nil +} + +func CopyQuorumEntrypointToVolume(ctx context.Context, quorumEntrypointDirectory, volumeName string) error { + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(quorumEntrypointDirectory, entrypoint), ""); err != nil { return err } return nil diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 56815ea3..6c004760 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -119,13 +119,14 @@ func (p *GethProvider) FirstTimeSetup() error { gethVolumeNameMember := fmt.Sprintf("%s_%d", gethVolumeName, i) tesseraVolumeNameMember := fmt.Sprintf("%s_%d", tesseraVolumeName, i) - // Copy the wallet files of all members to the blockchain volume (TODO) change to only relevant keys + // Copy the wallet files of all members to the blockchain volume + // TODO: change to only relevant keys keystoreDirectory := filepath.Join(blockchainDir, "keystore") if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, keystoreDirectory, "/"); err != nil { return err } - // Copy member specific tessera key files and docker entrypoint files to each of the tessera volume + // Copy member specific tessera key files to each of the tessera volume if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, tesseraDirWithinContainer); err != nil { return err } @@ -133,10 +134,6 @@ func (p *GethProvider) FirstTimeSetup() error { if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmDirectory, tesseraDirWithinContainer); err != nil { return err } - entrypointFile := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "docker-entrypoint.sh") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, entrypointFile, tesseraDirWithinContainer); err != nil { - return err - } // Copy the genesis block information if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { @@ -202,7 +199,6 @@ func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, } func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { - gethCommand := fmt.Sprintf(`--datadir /data --syncmode 'full' --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port 8545 --http.vhosts "*" --http.api 'admin,personal,eth,net,web3,txpool,miner,clique,debug' --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --nodiscover --verbosity 4 --miner.gaslimit 16777215`, p.stack.ChainID()) memberCount := len(p.stack.Members) serviceDefinitions := make([]*docker.ServiceDefinition, 2*memberCount) connectorDependents := map[string]string{} @@ -212,11 +208,11 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition Service: &docker.Service{ Image: gethImage, ContainerName: fmt.Sprintf("%s_geth_%d", p.stack.Name, i), - Command: gethCommand, Volumes: []string{fmt.Sprintf("geth_%d:/data", i)}, Logging: docker.StandardLogOptions, Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*10))}, // defaults 5100, 5110, 5120, 5130 Environment: p.stack.EnvironmentVars, + EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, DependsOn: map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}}, }, VolumeNames: []string{fmt.Sprintf("geth_%d", i)}, @@ -229,7 +225,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, Environment: p.stack.EnvironmentVars, - EntryPoint: []string{"/bin/sh", "-c", "/data/qdata/dd/docker-entrypoint.sh"}, + EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, }, VolumeNames: []string{fmt.Sprintf("tessera_%d", i)}, @@ -297,8 +293,8 @@ func (p *GethProvider) DeployContract(filename, contractName, instanceName strin func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { l := log.LoggerFromContext(p.ctx) - gethVolumeName := fmt.Sprintf("%s_geth_0", p.stack.Name) - tesseraVolumeName := fmt.Sprintf("%s_tessera_0", p.stack.Name) + gethVolumeName := fmt.Sprintf("%s_geth_%s", p.stack.Name, args[2]) + tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, args[2]) var directory string stackHasRunBefore, err := p.stack.HasRunBefore() if err != nil { @@ -322,10 +318,16 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) + l.Info("generating tessera entrypoint file") tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", args[2])) if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, args[3]); err != nil { return nil, err } + l.Info("generating quorum entrypoint file") + quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", args[2])) + if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, args[2], int(p.stack.ChainID())); err != nil { + return nil, err + } if stackHasRunBefore { if err := ethereum.CopyWalletFileToVolume(p.ctx, walletFilePath, gethVolumeName); err != nil { @@ -340,6 +342,9 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { if err := ethereum.CopyTesseraEntrypointToVolume(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName); err != nil { return nil, err } + if err := ethereum.CopyQuorumEntrypointToVolume(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName); err != nil { + return nil, err + } } return ðereum.Account{ diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index cfa60902..8ca9c10e 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -687,10 +687,15 @@ func (s *StackManager) PullStack(options *types.PullOptions) error { } // Use docker to pull every image - retry on failure + hasPulled := map[string]bool{} for _, image := range images { - s.Log.Info(fmt.Sprintf("pulling '%s'", image)) - if err := docker.RunDockerCommandRetry(s.ctx, s.Stack.InitDir, options.Retries, "pull", image); err != nil { - return err + if _, ok := hasPulled[image]; !ok { + s.Log.Info(fmt.Sprintf("pulling '%s'", image)) + if err := docker.RunDockerCommandRetry(s.ctx, s.Stack.InitDir, options.Retries, "pull", image); err != nil { + return err + } else { + hasPulled[image] = true + } } } return nil From 7df502dfaf24d958256d81301d3eabcd54de3d77 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 21 Jun 2024 02:14:28 +0000 Subject: [PATCH 06/22] Unlock only member specific accounts per quorum node Signed-off-by: rodion --- .../ethereum/quorum/geth_provider.go | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 6c004760..fc083239 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -38,6 +38,7 @@ import ( var gethImage = "quorumengineering/quorum:24.4" var tesseraImage = "quorumengineering/tessera:24.4" +var exposedBlockchainPortMultiplier = 10 // TODO: Probably randomize this and make it different per member? var keyPassword = "correcthorsebatterystaple" @@ -69,8 +70,7 @@ func (p *GethProvider) WriteConfig(options *types.InitOptions) error { for i, member := range p.stack.Members { // Generate the connector config for each member connectorConfigPath := filepath.Join(initDir, "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) - // TODO: remove hardcoding to geth_0 once we connect the quorum network together - if err := p.connector.GenerateConfig(p.stack, member, fmt.Sprintf("geth_%d", 0)).WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { + if err := p.connector.GenerateConfig(p.stack, member, fmt.Sprintf("geth_%d", i)).WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { return nil } } @@ -119,9 +119,8 @@ func (p *GethProvider) FirstTimeSetup() error { gethVolumeNameMember := fmt.Sprintf("%s_%d", gethVolumeName, i) tesseraVolumeNameMember := fmt.Sprintf("%s_%d", tesseraVolumeName, i) - // Copy the wallet files of all members to the blockchain volume - // TODO: change to only relevant keys - keystoreDirectory := filepath.Join(blockchainDir, "keystore") + // Copy the wallet files of each member to their respective blockchain volume + keystoreDirectory := filepath.Join(blockchainDir, fmt.Sprintf("geth_%d", i), "keystore") if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, keystoreDirectory, "/"); err != nil { return err } @@ -160,7 +159,14 @@ func (p *GethProvider) PostStart(firstTimeSetup bool) error { for _, account := range p.stack.State.Accounts { address := account.(*ethereum.Account).Address l.Info(fmt.Sprintf("unlocking account %s", address)) - if err := p.unlockAccount(address, keyPassword); err != nil { + // Check which member the account belongs to + var memberIndex int + for _, member := range p.stack.Members { + if member.Account.(*ethereum.Account).Address == address { + memberIndex = *member.Index + } + } + if err := p.unlockAccount(address, keyPassword, memberIndex); err != nil { return err } } @@ -168,10 +174,11 @@ func (p *GethProvider) PostStart(firstTimeSetup bool) error { return nil } -func (p *GethProvider) unlockAccount(address, password string) error { +func (p *GethProvider) unlockAccount(address, password string, memberIndex int) error { l := log.LoggerFromContext(p.ctx) verbose := log.VerbosityFromContext(p.ctx) - gethClient := NewGethClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort)) + // exposed blockchain port is the default for node 0, we need to add the port multiplier to get the right rpc for the correct node + gethClient := NewGethClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*exposedBlockchainPortMultiplier))) retries := 10 for { if err := gethClient.UnlockAccount(address, password); err != nil { @@ -210,7 +217,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition ContainerName: fmt.Sprintf("%s_geth_%d", p.stack.Name, i), Volumes: []string{fmt.Sprintf("geth_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*10))}, // defaults 5100, 5110, 5120, 5130 + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, DependsOn: map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}}, @@ -293,8 +300,10 @@ func (p *GethProvider) DeployContract(filename, contractName, instanceName strin func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { l := log.LoggerFromContext(p.ctx) - gethVolumeName := fmt.Sprintf("%s_geth_%s", p.stack.Name, args[2]) - tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, args[2]) + memberIndex := args[2] + memberCount := args[3] + gethVolumeName := fmt.Sprintf("%s_geth_%s", p.stack.Name, memberIndex) + tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) var directory string stackHasRunBefore, err := p.stack.HasRunBefore() if err != nil { @@ -307,25 +316,25 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { } prefix := strconv.FormatInt(time.Now().UnixNano(), 10) - outputDirectory := filepath.Join(directory, "blockchain", "keystore") + outputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex), "keystore") keyPair, walletFilePath, err := ethereum.CreateWalletFile(outputDirectory, prefix, keyPassword) if err != nil { return nil, err } - tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", args[2]), "keystore") + tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") tesseraKeysPath, err := ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) if err != nil { return nil, err } l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) l.Info("generating tessera entrypoint file") - tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", args[2])) - if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, args[3]); err != nil { + tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) + if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { return nil, err } l.Info("generating quorum entrypoint file") - quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", args[2])) - if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, args[2], int(p.stack.ChainID())); err != nil { + quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) + if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, int(p.stack.ChainID())); err != nil { return nil, err } @@ -333,8 +342,12 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { if err := ethereum.CopyWalletFileToVolume(p.ctx, walletFilePath, gethVolumeName); err != nil { return nil, err } - if err := p.unlockAccount(keyPair.Address.String(), keyPassword); err != nil { + if memberIndexInt, err := strconv.Atoi(memberIndex); err != nil { return nil, err + } else { + if err := p.unlockAccount(keyPair.Address.String(), keyPassword, memberIndexInt); err != nil { + return nil, err + } } if err := ethereum.CopyTesseraKeysToVolume(p.ctx, tesseraKeysOutputDirectory, tesseraVolumeName); err != nil { return nil, err From ddd52269b7d7a791fad9101e617dbfa7cd2142b6 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 21 Jun 2024 05:33:11 +0000 Subject: [PATCH 07/22] Store private transaction manager keys in stack state and add tests Signed-off-by: rodion --- internal/blockchain/ethereum/ethereum.go | 6 ++-- .../ethereum/private_transaction_manager.go | 34 ++++++++++++++++--- .../ethereum/quorum/geth_provider.go | 8 +++-- .../ethereum/quorum/geth_provider_test.go | 25 +++++++++----- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 80d4383e..7f143f70 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -32,8 +32,10 @@ import ( ) type Account struct { - Address string `json:"address"` - PrivateKey string `json:"privateKey"` + Address string `json:"address"` + PrivateKey string `json:"privateKey"` + PtmPublicKey string `json:"ptmPublicKey"` + PtmPrivateKey string `json:"ptmPrivateKey"` } func GenerateAddressAndPrivateKey() (address string, privateKey string) { diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index 968e6b15..1941a3c0 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -18,6 +18,7 @@ package ethereum import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -30,7 +31,16 @@ import ( var entrypoint = "docker-entrypoint.sh" var tmpath = "/qdata/tm" -func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name, password string) (string, error) { +type PrivateKeyData struct { + Bytes string `json:"bytes"` +} + +type PrivateKey struct { + Type string `json:"type"` + Data PrivateKeyData `json:"data"` +} + +func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name, password string) (privateKey, pubKey, path string, err error) { // generates both .pub and .key files used by Tessera var filename string if prefix != "" { @@ -39,14 +49,28 @@ func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name filename = name } if err := os.MkdirAll(outputDirectory, 0755); err != nil { - return "", err + return "", "", "", err } fmt.Println("generating tessera keys") - err := docker.RunDockerCommand(ctx, outputDirectory, "run", "--rm", "-v", fmt.Sprintf("%s:/keystore", outputDirectory), image, "-keygen", "-filename", fmt.Sprintf("/keystore/%s", filename)) + err = docker.RunDockerCommand(ctx, outputDirectory, "run", "--rm", "-v", fmt.Sprintf("%s:/keystore", outputDirectory), image, "-keygen", "-filename", fmt.Sprintf("/keystore/%s", filename)) + if err != nil { + return "", "", "", err + } + path = fmt.Sprintf("%s/%s", outputDirectory, filename) + pubKeyBytes, err := os.ReadFile(fmt.Sprintf("%v.%s", path, "pub")) + if err != nil { + return "", "", "", err + } + privateKeyBytes, err := os.ReadFile(fmt.Sprintf("%v.%s", path, "key")) + if err != nil { + return "", "", "", err + } + var privateKeyData PrivateKey + err = json.Unmarshal(privateKeyBytes, &privateKeyData) if err != nil { - return "", err + return "", "", "", err } - return fmt.Sprintf("%s/%s", outputDirectory, filename), nil + return privateKeyData.Data.Bytes, string(pubKeyBytes[:]), path, nil } func CopyTesseraKeysToVolume(ctx context.Context, tesseraKeyPath, volumeName string) error { diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index fc083239..6b898b49 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -322,7 +322,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") - tesseraKeysPath, err := ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + tesseraPrivateKey, tesseraPubKey, tesseraKeysPath, err := ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) if err != nil { return nil, err } @@ -361,8 +361,10 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { } return ðereum.Account{ - Address: keyPair.Address.String(), - PrivateKey: hex.EncodeToString(keyPair.PrivateKeyBytes()), + Address: keyPair.Address.String(), + PrivateKey: hex.EncodeToString(keyPair.PrivateKeyBytes()), + PtmPublicKey: tesseraPubKey, + PtmPrivateKey: tesseraPrivateKey, }, nil } diff --git a/internal/blockchain/ethereum/quorum/geth_provider_test.go b/internal/blockchain/ethereum/quorum/geth_provider_test.go index 954791d7..906b0ac7 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider_test.go +++ b/internal/blockchain/ethereum/quorum/geth_provider_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum" + "github.com/hyperledger/firefly-cli/internal/log" "github.com/hyperledger/firefly-cli/pkg/types" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/stretchr/testify/assert" @@ -47,7 +48,7 @@ func TestNewGethProvider(t *testing.T) { }, BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, { @@ -67,7 +68,7 @@ func TestNewGethProvider(t *testing.T) { }, BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Ethconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, } @@ -268,41 +269,43 @@ func TestGetConnectorExternal(t *testing.T) { } func TestCreateAccount(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testcases := []struct { Name string + Ctx context.Context Stack *types.Stack Args []string }{ { Name: "testcase1", + Ctx: ctx, Stack: &types.Stack{ Name: "Org-1_geth", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), }, - Args: []string{}, + Args: []string{"Org-1_geth", "Org-1_geth", "0", "1"}, }, { Name: "testcase1", + Ctx: ctx, Stack: &types.Stack{ Name: "Org-2_geth", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "geth"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), }, - Args: []string{}, + Args: []string{"Org-2_geth", "Org-2_geth", "1", "2"}, }, } for _, tc := range testcases { t.Run(tc.Name, func(t *testing.T) { - p := &GethProvider{ - stack: tc.Stack, - } + p := NewGethProvider(tc.Ctx, tc.Stack) Account, err := p.CreateAccount(tc.Args) if err != nil { t.Log("unable to create account", err) @@ -318,6 +321,10 @@ func TestCreateAccount(t *testing.T) { assert.NotEmpty(t, account.PrivateKey) _, err = hex.DecodeString(account.PrivateKey) assert.NoError(t, err, "invalid private key format") + // Check if the tessera private key is a non-empty string + assert.NotEmpty(t, account.PtmPrivateKey) + // Check if the tessera public key is a non-empty string + assert.NotEmpty(t, account.PtmPublicKey) }) } } From 8f0eefb60a609132aa043ddb66b4f5f7384d61ea Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 21 Jun 2024 07:31:17 +0000 Subject: [PATCH 08/22] Expose ptm base port as a cli flag Signed-off-by: rodion --- cmd/init.go | 1 + .../ethereum/private_transaction_manager.go | 27 ++++++++++++------- .../ethereum/quorum/geth_provider.go | 1 + internal/stacks/stack_manager.go | 1 + pkg/types/options.go | 1 + pkg/types/stack.go | 1 + 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 277de5b2..6c0db722 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -246,6 +246,7 @@ func randomHexString(length int) (string, error) { func init() { initCmd.PersistentFlags().IntVarP(&initOptions.FireFlyBasePort, "firefly-base-port", "p", 5000, "Mapped port base of FireFly core API (1 added for each member)") initCmd.PersistentFlags().IntVarP(&initOptions.ServicesBasePort, "services-base-port", "s", 5100, "Mapped port base of services (100 added for each member)") + initCmd.PersistentFlags().IntVar(&initOptions.PtmBasePort, "ptm-base-port", 4100, "Mapped port base of private transaction manager (10 added for each member)") initCmd.PersistentFlags().StringVarP(&initOptions.DatabaseProvider, "database", "d", "sqlite3", fmt.Sprintf("Database type to use. Options are: %v", fftypes.FFEnumValues(types.DatabaseSelection))) initCmd.Flags().StringVarP(&initOptions.BlockchainConnector, "blockchain-connector", "c", "evmconnect", fmt.Sprintf("Blockchain connector to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainConnector))) initCmd.Flags().StringVarP(&initOptions.BlockchainProvider, "blockchain-provider", "b", "ethereum", fmt.Sprintf("Blockchain to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainProvider))) diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index 1941a3c0..8f5fb02c 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -30,6 +30,10 @@ import ( var entrypoint = "docker-entrypoint.sh" var tmpath = "/qdata/tm" +var TmQ2tPort = "9101" +var TmTpPort = "9080" +var TmP2pPort = "9000" +var GethPort = "8545" type PrivateKeyData struct { Bytes string `json:"bytes"` @@ -111,13 +115,13 @@ cat < ${DDIR}/tessera-config-09.json { "app":"ThirdParty", "enabled": true, - "serverAddress": "http://$(hostname -i):9080", + "serverAddress": "http://$(hostname -i):%s", "communicationType" : "REST" }, { "app":"Q2T", "enabled": true, - "serverAddress": "unix:${DDIR}/tm.ipc", + "serverAddress": "http://$(hostname -i):%s", "sslConfig": { "tls": "OFF" }, @@ -126,7 +130,7 @@ cat < ${DDIR}/tessera-config-09.json { "app":"P2P", "enabled": true, - "serverAddress": "http://$(hostname -i):9000", + "serverAddress": "http://$(hostname -i):%s", "sslConfig": { "tls": "OFF" }, @@ -154,7 +158,7 @@ cat < ${DDIR}/tessera-config-09.json } EOF /tessera/bin/tessera -configfile ${DDIR}/tessera-config-09.json -`, peerList) +`, TmTpPort, TmQ2tPort, TmP2pPort, peerList) filename := filepath.Join(outputDirectory, entrypoint) file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { @@ -182,9 +186,9 @@ func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirecto func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex string, chainId int) error { discoveryCmd := "BOOTNODE_CMD=\"\"" if memberIndex != "0" { - discoveryCmd = `bootnode=$(curl http://geth_0:8545 -s --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") + discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") BOOTNODE_CMD="--bootnodes $bootnode" -BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}` +BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort) } content := fmt.Sprintf(`#!/bin/sh @@ -217,11 +221,14 @@ then export QUORUM_API="clique" fi -TESSERA_URL=http://member%stessera:9000/upcheck -ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" +TESSERA_URL=http://member%stessera +TESSERA_TP_PORT=%s +TESSERA_Q2T_PORT=%s +TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck +ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" echo -n "Checking tessera is up ... " -curl --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_URL" +curl --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" echo "" # discovery @@ -229,7 +236,7 @@ echo "" echo "bootnode discovery command :: $BOOTNODE_CMD" IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port 8545 --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD`, memberIndex, discoveryCmd, chainId) +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, memberIndex, TmTpPort, TmQ2tPort, discoveryCmd, GethPort, chainId) filename := filepath.Join(outputDirectory, entrypoint) if err := os.MkdirAll(outputDirectory, 0755); err != nil { return err diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 6b898b49..71813360 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -231,6 +231,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition ContainerName: fmt.Sprintf("member%dtessera", i), Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, + Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), ethereum.TmTpPort)}, // defaults 4100, 4110, 4120, 4130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 8ca9c10e..0c64d446 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -98,6 +98,7 @@ func (s *StackManager) InitStack(options *types.InitOptions) (err error) { Name: options.StackName, Members: make([]*types.Organization, options.MemberCount), ExposedBlockchainPort: options.ServicesBasePort, + ExposedPtmPort: options.PtmBasePort, Database: fftypes.FFEnum(options.DatabaseProvider), BlockchainProvider: fftypes.FFEnum(options.BlockchainProvider), BlockchainNodeProvider: fftypes.FFEnum(options.BlockchainNodeProvider), diff --git a/pkg/types/options.go b/pkg/types/options.go index f1579c8f..72803ccf 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -35,6 +35,7 @@ type InitOptions struct { MemberCount int FireFlyBasePort int ServicesBasePort int + PtmBasePort int DatabaseProvider string ExternalProcesses int OrgNames []string diff --git a/pkg/types/stack.go b/pkg/types/stack.go index 5d366ddc..72d0e0aa 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -29,6 +29,7 @@ type Stack struct { Members []*Organization `json:"members,omitempty"` SwarmKey string `json:"swarmKey,omitempty"` ExposedBlockchainPort int `json:"exposedBlockchainPort,omitempty"` + ExposedPtmPort int `json:"exposedPtmPort,omitempty"` Database fftypes.FFEnum `json:"database"` BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` From fe36fcbeff915783df77ca8bc87400a3a8746ea0 Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 27 Jun 2024 08:35:36 +0000 Subject: [PATCH 09/22] Add tesseraEnabled flag and explicitly only allow quorum with tessera pair Signed-off-by: rodion --- cmd/init.go | 18 +++ .../ethereum/private_transaction_manager.go | 37 +++--- .../ethereum/quorum/geth_provider.go | 106 +++++++++++------- internal/stacks/stack_manager.go | 1 + pkg/types/options.go | 1 + pkg/types/stack.go | 1 + 6 files changed, 107 insertions(+), 57 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 6c0db722..af171e20 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -82,6 +82,9 @@ func initCommon(args []string) error { if err := validateIPFSMode(initOptions.IPFSMode); err != nil { return err } + if err := validateTesseraSelection(initOptions.TesseraEnabled, initOptions.BlockchainNodeProvider); err != nil { + return err + } fmt.Println("initializing new FireFly stack...") @@ -200,6 +203,20 @@ func validateBlockchainProvider(providerString, nodeString string) error { return nil } +func validateTesseraSelection(tesseraEnabled bool, nodeString string) error { + if tesseraEnabled { + v, err := fftypes.FFEnumParseString(context.Background(), types.BlockchainNodeProvider, nodeString) + if err != nil { + return nil + } + + if v != types.BlockchainNodeProviderQuorum { + return errors.New("tessera can only be enabled if blockchain node provider is quorum") + } + } + return nil +} + func validateTokensProvider(input []string, blockchainNodeProviderInput string) error { tokenProviders := make([]fftypes.FFEnum, len(input)) for i, t := range input { @@ -251,6 +268,7 @@ func init() { initCmd.Flags().StringVarP(&initOptions.BlockchainConnector, "blockchain-connector", "c", "evmconnect", fmt.Sprintf("Blockchain connector to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainConnector))) initCmd.Flags().StringVarP(&initOptions.BlockchainProvider, "blockchain-provider", "b", "ethereum", fmt.Sprintf("Blockchain to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainProvider))) initCmd.Flags().StringVarP(&initOptions.BlockchainNodeProvider, "blockchain-node", "n", "geth", fmt.Sprintf("Blockchain node type to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainNodeProvider))) + initCmd.PersistentFlags().BoolVar(&initOptions.TesseraEnabled, "tessera-enabled", false, "Enables private transaction manager Tessera to start alongside with Quorum") initCmd.PersistentFlags().StringArrayVarP(&initOptions.TokenProviders, "token-providers", "t", []string{"erc20_erc721"}, fmt.Sprintf("Token providers to use. Options are: %v", fftypes.FFEnumValues(types.TokenProvider))) initCmd.PersistentFlags().IntVarP(&initOptions.ExternalProcesses, "external", "e", 0, "Manage a number of FireFly core processes outside of the docker-compose stack - useful for development and debugging") initCmd.PersistentFlags().StringVarP(&initOptions.FireFlyVersion, "release", "r", "latest", fmt.Sprintf("Select the FireFly release version to use. Options are: %v", fftypes.FFEnumValues(types.ReleaseChannelSelection))) diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index 8f5fb02c..d058d4d7 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -95,13 +95,12 @@ func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, m var sb strings.Builder memberCountInt, _ := strconv.Atoi(memberCount) for i := 0; i < memberCountInt; i++ { - sb.WriteString(fmt.Sprintf("{\"url\":\"http://member%dtessera:9000\"},", i)) // construct peer list + sb.WriteString(fmt.Sprintf("{\"url\":\"http://member%dtessera:%s\"},", i, TmP2pPort)) // construct peer list } peerList := strings.TrimSuffix(sb.String(), ",") content := fmt.Sprintf(`export JAVA_OPTS="-Xms128M -Xmx128M" DDIR=/data/qdata/dd mkdir -p ${DDIR} -[ -f ${DDIR}/dd/tm.ipc ] && rm -f ${DDIR}/dd/tm.ipc cat < ${DDIR}/tessera-config-09.json { "useWhiteList": false, @@ -183,12 +182,27 @@ func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirecto return nil } -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex string, chainId int) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex string, chainId int, tesseraEnabled bool) error { discoveryCmd := "BOOTNODE_CMD=\"\"" + connectTimeout := 15 if memberIndex != "0" { - discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") + discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout %[2]d --max-time %[2]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") BOOTNODE_CMD="--bootnodes $bootnode" -BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort) +BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort, connectTimeout) + } + + tesseraCmd := "" + if tesseraEnabled { + tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://member%stessera +TESSERA_TP_PORT=%s +TESSERA_Q2T_PORT=%s +TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck +ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" + +echo -n "Checking tessera is up ... " +curl --connect-timeout %[4]d --max-time %[4]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" +echo "" +`, memberIndex, TmTpPort, TmQ2tPort, connectTimeout) } content := fmt.Sprintf(`#!/bin/sh @@ -221,22 +235,15 @@ then export QUORUM_API="clique" fi -TESSERA_URL=http://member%stessera -TESSERA_TP_PORT=%s -TESSERA_Q2T_PORT=%s -TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck -ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" - -echo -n "Checking tessera is up ... " -curl --connect-timeout 5 --max-time 10 --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" -echo "" +ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} +%s # discovery %s echo "bootnode discovery command :: $BOOTNODE_CMD" IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, memberIndex, TmTpPort, TmQ2tPort, discoveryCmd, GethPort, chainId) +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, tesseraCmd, discoveryCmd, GethPort, chainId) filename := filepath.Join(outputDirectory, entrypoint) if err := os.MkdirAll(outputDirectory, 0755); err != nil { return err diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 71813360..0210023f 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -125,13 +125,15 @@ func (p *GethProvider) FirstTimeSetup() error { return err } - // Copy member specific tessera key files to each of the tessera volume - if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, tesseraDirWithinContainer); err != nil { - return err - } - tmDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmDirectory, tesseraDirWithinContainer); err != nil { - return err + if p.stack.TesseraEnabled { + // Copy member specific tessera key files to each of the tessera volume + if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, tesseraDirWithinContainer); err != nil { + return err + } + tmDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmDirectory, tesseraDirWithinContainer); err != nil { + return err + } } // Copy the genesis block information @@ -207,9 +209,17 @@ func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { memberCount := len(p.stack.Members) - serviceDefinitions := make([]*docker.ServiceDefinition, 2*memberCount) + serviceDefinitionsCount := memberCount + if p.stack.TesseraEnabled { + serviceDefinitionsCount *= 2 + } + serviceDefinitions := make([]*docker.ServiceDefinition, serviceDefinitionsCount) connectorDependents := map[string]string{} for i := 0; i < memberCount; i++ { + var dependsOn map[string]map[string]string + if p.stack.TesseraEnabled { + dependsOn = map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}} + } serviceDefinitions[i] = &docker.ServiceDefinition{ ServiceName: fmt.Sprintf("geth_%d", i), Service: &docker.Service{ @@ -220,25 +230,28 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, - DependsOn: map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}}, + DependsOn: dependsOn, }, VolumeNames: []string{fmt.Sprintf("geth_%d", i)}, } - serviceDefinitions[i+memberCount] = &docker.ServiceDefinition{ - ServiceName: fmt.Sprintf("tessera_%d", i), - Service: &docker.Service{ - Image: tesseraImage, - ContainerName: fmt.Sprintf("member%dtessera", i), - Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, - Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), ethereum.TmTpPort)}, // defaults 4100, 4110, 4120, 4130 - Environment: p.stack.EnvironmentVars, - EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, - Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, - }, - VolumeNames: []string{fmt.Sprintf("tessera_%d", i)}, - } connectorDependents[fmt.Sprintf("geth_%d", i)] = "service_started" + + if p.stack.TesseraEnabled { + serviceDefinitions[i+memberCount] = &docker.ServiceDefinition{ + ServiceName: fmt.Sprintf("tessera_%d", i), + Service: &docker.Service{ + Image: tesseraImage, + ContainerName: fmt.Sprintf("member%dtessera", i), + Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, + Logging: docker.StandardLogOptions, + Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), ethereum.TmTpPort)}, // defaults 4100, 4110, 4120, 4130 + Environment: p.stack.EnvironmentVars, + EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, + Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, + }, + VolumeNames: []string{fmt.Sprintf("tessera_%d", i)}, + } + } } serviceDefinitions = append(serviceDefinitions, p.connector.GetServiceDefinitions(p.stack, connectorDependents)...) return serviceDefinitions @@ -304,7 +317,6 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { memberIndex := args[2] memberCount := args[3] gethVolumeName := fmt.Sprintf("%s_geth_%s", p.stack.Name, memberIndex) - tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) var directory string stackHasRunBefore, err := p.stack.HasRunBefore() if err != nil { @@ -322,20 +334,36 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { if err != nil { return nil, err } - tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") - tesseraPrivateKey, tesseraPubKey, tesseraKeysPath, err := ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) - if err != nil { - return nil, err - } - l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) - l.Info("generating tessera entrypoint file") - tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) - if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { - return nil, err + + // Tessera is an optional add-on to the quorum blockchain node provider + var tesseraPrivateKey, tesseraPubKey, tesseraKeysPath string + if p.stack.TesseraEnabled { + tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) + tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") + tesseraPrivateKey, tesseraPubKey, tesseraKeysPath, err = ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + if err != nil { + return nil, err + } + l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) + l.Info(fmt.Sprintln("generating tessera entrypoint file for member", memberIndex)) + tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) + if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { + return nil, err + } + + if stackHasRunBefore { + if err := ethereum.CopyTesseraKeysToVolume(p.ctx, tesseraKeysOutputDirectory, tesseraVolumeName); err != nil { + return nil, err + } + if err := ethereum.CopyTesseraEntrypointToVolume(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName); err != nil { + return nil, err + } + } } - l.Info("generating quorum entrypoint file") + + l.Info(fmt.Sprintf("generating quorum entrypoint file for member %s", memberIndex)) quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) - if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, int(p.stack.ChainID())); err != nil { + if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { return nil, err } @@ -350,12 +378,6 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } } - if err := ethereum.CopyTesseraKeysToVolume(p.ctx, tesseraKeysOutputDirectory, tesseraVolumeName); err != nil { - return nil, err - } - if err := ethereum.CopyTesseraEntrypointToVolume(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName); err != nil { - return nil, err - } if err := ethereum.CopyQuorumEntrypointToVolume(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName); err != nil { return nil, err } diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 0c64d446..76da51cf 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -103,6 +103,7 @@ func (s *StackManager) InitStack(options *types.InitOptions) (err error) { BlockchainProvider: fftypes.FFEnum(options.BlockchainProvider), BlockchainNodeProvider: fftypes.FFEnum(options.BlockchainNodeProvider), BlockchainConnector: fftypes.FFEnum(options.BlockchainConnector), + TesseraEnabled: options.TesseraEnabled, ContractAddress: options.ContractAddress, StackDir: filepath.Join(constants.StacksDir, options.StackName), InitDir: filepath.Join(constants.StacksDir, options.StackName, "init"), diff --git a/pkg/types/options.go b/pkg/types/options.go index 72803ccf..c594d480 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -43,6 +43,7 @@ type InitOptions struct { BlockchainConnector string BlockchainProvider string BlockchainNodeProvider string + TesseraEnabled bool TokenProviders []string FireFlyVersion string ManifestPath string diff --git a/pkg/types/stack.go b/pkg/types/stack.go index 72d0e0aa..bef9a619 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -34,6 +34,7 @@ type Stack struct { BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` + TesseraEnabled bool `json:"tesseraEnabled"` TokenProviders []fftypes.FFEnum `json:"tokenProviders"` VersionManifest *VersionManifest `json:"versionManifest,omitempty"` PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` From 91c6a1f0725c5f6ec652009cfc6f70712ae9d63f Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 27 Jun 2024 09:22:44 +0000 Subject: [PATCH 10/22] Make quorum consensus configurable via cli flag Signed-off-by: rodion --- cmd/init.go | 17 +++++++++++++++++ .../ethereum/private_transaction_manager.go | 6 +++--- .../blockchain/ethereum/quorum/geth_provider.go | 4 ++-- internal/stacks/stack_manager.go | 1 + pkg/types/options.go | 10 ++++++++++ pkg/types/stack.go | 1 + 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index af171e20..0e8e8dfb 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -82,6 +82,9 @@ func initCommon(args []string) error { if err := validateIPFSMode(initOptions.IPFSMode); err != nil { return err } + if err := validateQuorumConsensus(initOptions.QuorumConsensus); err != nil { + return err + } if err := validateTesseraSelection(initOptions.TesseraEnabled, initOptions.BlockchainNodeProvider); err != nil { return err } @@ -203,6 +206,19 @@ func validateBlockchainProvider(providerString, nodeString string) error { return nil } +func validateQuorumConsensus(consensusString string) error { + v, err := fftypes.FFEnumParseString(context.Background(), types.QuorumConsensus, consensusString) + if err != nil { + return nil + } + + if v != types.QuorumConsensusClique { + return errors.New("support for raft/ibft/qbft will come in future") + } + + return nil +} + func validateTesseraSelection(tesseraEnabled bool, nodeString string) error { if tesseraEnabled { v, err := fftypes.FFEnumParseString(context.Background(), types.BlockchainNodeProvider, nodeString) @@ -269,6 +285,7 @@ func init() { initCmd.Flags().StringVarP(&initOptions.BlockchainProvider, "blockchain-provider", "b", "ethereum", fmt.Sprintf("Blockchain to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainProvider))) initCmd.Flags().StringVarP(&initOptions.BlockchainNodeProvider, "blockchain-node", "n", "geth", fmt.Sprintf("Blockchain node type to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainNodeProvider))) initCmd.PersistentFlags().BoolVar(&initOptions.TesseraEnabled, "tessera-enabled", false, "Enables private transaction manager Tessera to start alongside with Quorum") + initCmd.PersistentFlags().StringVar(&initOptions.QuorumConsensus, "quorum-consensus", "clique", fmt.Sprintf("Consensus algorithm used when Blockchain node type is Quorum. Options are %v", fftypes.FFEnumValues(types.QuorumConsensus))) initCmd.PersistentFlags().StringArrayVarP(&initOptions.TokenProviders, "token-providers", "t", []string{"erc20_erc721"}, fmt.Sprintf("Token providers to use. Options are: %v", fftypes.FFEnumValues(types.TokenProvider))) initCmd.PersistentFlags().IntVarP(&initOptions.ExternalProcesses, "external", "e", 0, "Manage a number of FireFly core processes outside of the docker-compose stack - useful for development and debugging") initCmd.PersistentFlags().StringVarP(&initOptions.FireFlyVersion, "release", "r", "latest", fmt.Sprintf("Select the FireFly release version to use. Options are: %v", fftypes.FFEnumValues(types.ReleaseChannelSelection))) diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index d058d4d7..2ca9aa8d 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -182,7 +182,7 @@ func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirecto return nil } -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex string, chainId int, tesseraEnabled bool) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus string, chainId int, tesseraEnabled bool) error { discoveryCmd := "BOOTNODE_CMD=\"\"" connectTimeout := 15 if memberIndex != "0" { @@ -212,7 +212,7 @@ set -o nounset set -o pipefail set -o xtrace -GOQUORUM_CONS_ALGO=$(echo "${GOQUORUM_CONS_ALGO:-clique}" | tr '[:lower:]') +GOQUORUM_CONS_ALGO=%s if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; then echo "Using istanbul for consensus algorithm..." @@ -243,7 +243,7 @@ ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} echo "bootnode discovery command :: $BOOTNODE_CMD" IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, tesseraCmd, discoveryCmd, GethPort, chainId) +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, GethPort, chainId) filename := filepath.Join(outputDirectory, entrypoint) if err := os.MkdirAll(outputDirectory, 0755); err != nil { return err diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 0210023f..962d3269 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -345,7 +345,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) - l.Info(fmt.Sprintln("generating tessera entrypoint file for member", memberIndex)) + l.Info(fmt.Sprintf("generating tessera entrypoint file for member %s", memberIndex)) tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { return nil, err @@ -363,7 +363,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { l.Info(fmt.Sprintf("generating quorum entrypoint file for member %s", memberIndex)) quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) - if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { + if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { return nil, err } diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 76da51cf..626f822e 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -104,6 +104,7 @@ func (s *StackManager) InitStack(options *types.InitOptions) (err error) { BlockchainNodeProvider: fftypes.FFEnum(options.BlockchainNodeProvider), BlockchainConnector: fftypes.FFEnum(options.BlockchainConnector), TesseraEnabled: options.TesseraEnabled, + QuorumConsensus: fftypes.FFEnum(options.QuorumConsensus), ContractAddress: options.ContractAddress, StackDir: filepath.Join(constants.StacksDir, options.StackName), InitDir: filepath.Join(constants.StacksDir, options.StackName, "init"), diff --git a/pkg/types/options.go b/pkg/types/options.go index c594d480..9633ceea 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -44,6 +44,7 @@ type InitOptions struct { BlockchainProvider string BlockchainNodeProvider string TesseraEnabled bool + QuorumConsensus string TokenProviders []string FireFlyVersion string ManifestPath string @@ -104,6 +105,15 @@ var ( BlockchainNodeProviderRemoteRPC = fftypes.FFEnumValue(BlockchainNodeProvider, "remote-rpc") ) +const QuorumConsensus = "quorum_consensus" + +var ( + QuorumConsensusClique = fftypes.FFEnumValue(QuorumConsensus, "clique") + QuorumConsensusRaft = fftypes.FFEnumValue(QuorumConsensus, "raft") + QuorumConsensusIbft = fftypes.FFEnumValue(QuorumConsensus, "ibft") + QuorumConsensusQbft = fftypes.FFEnumValue(QuorumConsensus, "qbft") +) + const DatabaseSelection = "database_selection" var ( diff --git a/pkg/types/stack.go b/pkg/types/stack.go index bef9a619..3581baff 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -35,6 +35,7 @@ type Stack struct { BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` TesseraEnabled bool `json:"tesseraEnabled"` + QuorumConsensus fftypes.FFEnum `json:"quorumConsensus"` TokenProviders []fftypes.FFEnum `json:"tokenProviders"` VersionManifest *VersionManifest `json:"versionManifest,omitempty"` PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` From a8910c151b4f12adb476e176046f44b0c6b3c21c Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 27 Jun 2024 09:52:31 +0000 Subject: [PATCH 11/22] Remove private ptm private key references during stack creation Signed-off-by: rodion --- internal/blockchain/ethereum/ethereum.go | 7 ++--- .../ethereum/quorum/geth_provider.go | 22 ++++++++----- .../ethereum/quorum/geth_provider_test.go | 31 ++++++++++++++----- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 7f143f70..6951d6b5 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -32,10 +32,9 @@ import ( ) type Account struct { - Address string `json:"address"` - PrivateKey string `json:"privateKey"` - PtmPublicKey string `json:"ptmPublicKey"` - PtmPrivateKey string `json:"ptmPrivateKey"` + Address string `json:"address"` + PrivateKey string `json:"privateKey"` + PtmPublicKey string `json:"ptmPublicKey"` } func GenerateAddressAndPrivateKey() (address string, privateKey string) { diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 962d3269..6f1b265c 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -336,11 +336,11 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { } // Tessera is an optional add-on to the quorum blockchain node provider - var tesseraPrivateKey, tesseraPubKey, tesseraKeysPath string + var tesseraPubKey, tesseraKeysPath string if p.stack.TesseraEnabled { tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") - tesseraPrivateKey, tesseraPubKey, tesseraKeysPath, err = ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + _, tesseraPubKey, tesseraKeysPath, err = ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) if err != nil { return nil, err } @@ -384,18 +384,24 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { } return ðereum.Account{ - Address: keyPair.Address.String(), - PrivateKey: hex.EncodeToString(keyPair.PrivateKeyBytes()), - PtmPublicKey: tesseraPubKey, - PtmPrivateKey: tesseraPrivateKey, + Address: keyPair.Address.String(), + PrivateKey: hex.EncodeToString(keyPair.PrivateKeyBytes()), + PtmPublicKey: tesseraPubKey, }, nil } func (p *GethProvider) ParseAccount(account interface{}) interface{} { accountMap := account.(map[string]interface{}) + ptmPublicKey := "" // if we start quorum without tessera, no public key will be generated + v, ok := accountMap["ptmPublicKey"] + if ok { + ptmPublicKey = v.(string) + } + return ðereum.Account{ - Address: accountMap["address"].(string), - PrivateKey: accountMap["privateKey"].(string), + Address: accountMap["address"].(string), + PrivateKey: accountMap["privateKey"].(string), + PtmPublicKey: ptmPublicKey, } } diff --git a/internal/blockchain/ethereum/quorum/geth_provider_test.go b/internal/blockchain/ethereum/quorum/geth_provider_test.go index 906b0ac7..136d151d 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider_test.go +++ b/internal/blockchain/ethereum/quorum/geth_provider_test.go @@ -89,16 +89,31 @@ func TestParseAccount(t *testing.T) { { Name: "Account 1", Address: map[string]interface{}{ - "address": "0x1234567890abcdef0123456789abcdef6789abcd", - "privateKey": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + "address": "0x1234567890abcdef0123456789abcdef6789abcd", + "privateKey": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + "ptmPublicKey": "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", }, ExpectedAccount: ðereum.Account{ - Address: "0x1234567890abcdef0123456789abcdef6789abcd", - PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", }, }, { Name: "Account 2", + Address: map[string]interface{}{ + "address": "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", + "privateKey": "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + "ptmPublicKey": "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + ExpectedAccount: ðereum.Account{ + Address: "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", + PrivateKey: "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Name: "Account 3", Address: map[string]interface{}{ "address": "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", "privateKey": "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", @@ -321,10 +336,12 @@ func TestCreateAccount(t *testing.T) { assert.NotEmpty(t, account.PrivateKey) _, err = hex.DecodeString(account.PrivateKey) assert.NoError(t, err, "invalid private key format") - // Check if the tessera private key is a non-empty string - assert.NotEmpty(t, account.PtmPrivateKey) // Check if the tessera public key is a non-empty string - assert.NotEmpty(t, account.PtmPublicKey) + if tc.Stack.TesseraEnabled { + assert.NotEmpty(t, account.PtmPublicKey) + } else { + assert.Empty(t, account.PtmPublicKey) + } }) } } From 23480c50a475706b850e2373d50685c0b5ce6520 Mon Sep 17 00:00:00 2001 From: rodion Date: Thu, 27 Jun 2024 10:26:15 +0000 Subject: [PATCH 12/22] Remove duplicate dd for tessera Signed-off-by: rodion --- .../ethereum/private_transaction_manager.go | 16 +--------------- .../blockchain/ethereum/quorum/geth_provider.go | 11 +---------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/private_transaction_manager.go index 2ca9aa8d..64ae6933 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/private_transaction_manager.go @@ -29,7 +29,6 @@ import ( ) var entrypoint = "docker-entrypoint.sh" -var tmpath = "/qdata/tm" var TmQ2tPort = "9101" var TmTpPort = "9080" var TmP2pPort = "9000" @@ -77,19 +76,6 @@ func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name return privateKeyData.Data.Bytes, string(pubKeyBytes[:]), path, nil } -func CopyTesseraKeysToVolume(ctx context.Context, tesseraKeyPath, volumeName string) error { - if err := docker.MkdirInVolume(ctx, volumeName, tmpath); err != nil { - return err - } - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraKeyPath, "tm.pub"), tmpath); err != nil { - return err - } - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraKeyPath, "tm.key"), tmpath); err != nil { - return err - } - return nil -} - func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, memberCount string) error { // only tessera v09 onwards is supported var sb strings.Builder @@ -99,7 +85,7 @@ func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, m } peerList := strings.TrimSuffix(sb.String(), ",") content := fmt.Sprintf(`export JAVA_OPTS="-Xms128M -Xmx128M" -DDIR=/data/qdata/dd +DDIR=/data mkdir -p ${DDIR} cat < ${DDIR}/tessera-config-09.json { diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 6f1b265c..1bd2bedd 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -95,7 +95,7 @@ func (p *GethProvider) FirstTimeSetup() error { tesseraVolumeName := fmt.Sprintf("%s_tessera", p.stack.Name) blockchainDir := path.Join(p.stack.RuntimeDir, "blockchain") tesseraDir := path.Join(p.stack.RuntimeDir, "tessera") - tesseraDirWithinContainer := "/qdata/dd" + tesseraDirWithinContainer := "/" contractsDir := path.Join(p.stack.RuntimeDir, "contracts") if err := p.connector.FirstTimeSetup(p.stack); err != nil { @@ -350,15 +350,6 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { return nil, err } - - if stackHasRunBefore { - if err := ethereum.CopyTesseraKeysToVolume(p.ctx, tesseraKeysOutputDirectory, tesseraVolumeName); err != nil { - return nil, err - } - if err := ethereum.CopyTesseraEntrypointToVolume(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName); err != nil { - return nil, err - } - } } l.Info(fmt.Sprintf("generating quorum entrypoint file for member %s", memberIndex)) From e780adfafebee9696a02b2ef273d3630397619b9 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 28 Jun 2024 01:52:40 +0000 Subject: [PATCH 13/22] Refactor quorum docker entrypoint creation to its own file Signed-off-by: rodion --- .../ethereum/quorum/geth_provider.go | 14 +-- .../private_transaction_manager.go | 88 +------------- internal/blockchain/ethereum/quorum/quorum.go | 112 ++++++++++++++++++ 3 files changed, 120 insertions(+), 94 deletions(-) rename internal/blockchain/ethereum/{ => quorum}/private_transaction_manager.go (56%) create mode 100644 internal/blockchain/ethereum/quorum/quorum.go diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index 1bd2bedd..bcc52262 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -244,7 +244,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition ContainerName: fmt.Sprintf("member%dtessera", i), Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), ethereum.TmTpPort)}, // defaults 4100, 4110, 4120, 4130 + Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, @@ -340,21 +340,21 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { if p.stack.TesseraEnabled { tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") - _, tesseraPubKey, tesseraKeysPath, err = ethereum.CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + _, tesseraPubKey, tesseraKeysPath, err = CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) if err != nil { return nil, err } l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) - l.Info(fmt.Sprintf("generating tessera entrypoint file for member %s", memberIndex)) + l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %s", memberIndex)) tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) - if err := ethereum.CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { + if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { return nil, err } } - l.Info(fmt.Sprintf("generating quorum entrypoint file for member %s", memberIndex)) + l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %s", memberIndex)) quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) - if err := ethereum.CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { + if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { return nil, err } @@ -369,7 +369,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } } - if err := ethereum.CopyQuorumEntrypointToVolume(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName); err != nil { + if err := CopyQuorumEntrypointToVolume(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName); err != nil { return nil, err } } diff --git a/internal/blockchain/ethereum/private_transaction_manager.go b/internal/blockchain/ethereum/quorum/private_transaction_manager.go similarity index 56% rename from internal/blockchain/ethereum/private_transaction_manager.go rename to internal/blockchain/ethereum/quorum/private_transaction_manager.go index 64ae6933..a5587375 100644 --- a/internal/blockchain/ethereum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ethereum +package quorum import ( "context" @@ -167,89 +167,3 @@ func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirecto } return nil } - -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus string, chainId int, tesseraEnabled bool) error { - discoveryCmd := "BOOTNODE_CMD=\"\"" - connectTimeout := 15 - if memberIndex != "0" { - discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout %[2]d --max-time %[2]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") -BOOTNODE_CMD="--bootnodes $bootnode" -BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort, connectTimeout) - } - - tesseraCmd := "" - if tesseraEnabled { - tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://member%stessera -TESSERA_TP_PORT=%s -TESSERA_Q2T_PORT=%s -TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck -ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" - -echo -n "Checking tessera is up ... " -curl --connect-timeout %[4]d --max-time %[4]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" -echo "" -`, memberIndex, TmTpPort, TmQ2tPort, connectTimeout) - } - - content := fmt.Sprintf(`#!/bin/sh - -set -o errexit -set -o nounset -set -o pipefail -set -o xtrace - -GOQUORUM_CONS_ALGO=%s -if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; -then - echo "Using istanbul for consensus algorithm..." - export CONSENSUS_ARGS="--istanbul.blockperiod 5 --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" - export QUORUM_API="istanbul" -elif [ "qbft" == "$GOQUORUM_CONS_ALGO" ]; -then - echo "Using qbft for consensus algorithm..." - export CONSENSUS_ARGS="--mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" - export QUORUM_API="istanbul" -elif [ "raft" == "$GOQUORUM_CONS_ALGO" ]; -then - echo "Using raft for consensus algorithm..." - export CONSENSUS_ARGS="--raft --raftblocktime 300 --raftport 53000" - export QUORUM_API="raft" -elif [ "clique" == "$GOQUORUM_CONS_ALGO" ]; -then - echo "Using clique for consensus algorithm..." - export CONSENSUS_ARGS="" - export QUORUM_API="clique" -fi - -ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} -%s - -# discovery -%s -echo "bootnode discovery command :: $BOOTNODE_CMD" -IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') - -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, GethPort, chainId) - filename := filepath.Join(outputDirectory, entrypoint) - if err := os.MkdirAll(outputDirectory, 0755); err != nil { - return err - } - file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - return err - } - defer file.Close() - _, err = file.WriteString(content) - if err != nil { - return err - } - CopyQuorumEntrypointToVolume(ctx, outputDirectory, volumeName) - return nil -} - -func CopyQuorumEntrypointToVolume(ctx context.Context, quorumEntrypointDirectory, volumeName string) error { - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(quorumEntrypointDirectory, entrypoint), ""); err != nil { - return err - } - return nil -} diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go new file mode 100644 index 00000000..0cbd623e --- /dev/null +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -0,0 +1,112 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package quorum + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/hyperledger/firefly-cli/internal/docker" +) + +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus string, chainId int, tesseraEnabled bool) error { + discoveryCmd := "BOOTNODE_CMD=\"\"" + connectTimeout := 15 + if memberIndex != "0" { + discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout %[2]d --max-time %[2]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") +BOOTNODE_CMD="--bootnodes $bootnode" +BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort, connectTimeout) + } + + tesseraCmd := "" + if tesseraEnabled { + tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://member%stessera +TESSERA_TP_PORT=%s +TESSERA_Q2T_PORT=%s +TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck +ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" + +echo -n "Checking tessera is up ... " +curl --connect-timeout %[4]d --max-time %[4]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" +echo "" +`, memberIndex, TmTpPort, TmQ2tPort, connectTimeout) + } + + content := fmt.Sprintf(`#!/bin/sh + +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace + +GOQUORUM_CONS_ALGO=%s +if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using istanbul for consensus algorithm..." + export CONSENSUS_ARGS="--istanbul.blockperiod 5 --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" + export QUORUM_API="istanbul" +elif [ "qbft" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using qbft for consensus algorithm..." + export CONSENSUS_ARGS="--mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" + export QUORUM_API="istanbul" +elif [ "raft" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using raft for consensus algorithm..." + export CONSENSUS_ARGS="--raft --raftblocktime 300 --raftport 53000" + export QUORUM_API="raft" +elif [ "clique" == "$GOQUORUM_CONS_ALGO" ]; +then + echo "Using clique for consensus algorithm..." + export CONSENSUS_ARGS="" + export QUORUM_API="clique" +fi + +ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} +%s + +# discovery +%s +echo "bootnode discovery command :: $BOOTNODE_CMD" +IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') + +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, GethPort, chainId) + filename := filepath.Join(outputDirectory, entrypoint) + if err := os.MkdirAll(outputDirectory, 0755); err != nil { + return err + } + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content) + if err != nil { + return err + } + CopyQuorumEntrypointToVolume(ctx, outputDirectory, volumeName) + return nil +} + +func CopyQuorumEntrypointToVolume(ctx context.Context, quorumEntrypointDirectory, volumeName string) error { + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(quorumEntrypointDirectory, entrypoint), ""); err != nil { + return err + } + return nil +} From 1589b82a678eef7f4ffe66b97baa6fc89f7ccb88 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 28 Jun 2024 02:36:53 +0000 Subject: [PATCH 14/22] Check that Tessera ports are available Signed-off-by: rodion --- internal/stacks/stack_manager.go | 3 +++ pkg/types/organization.go | 1 + 2 files changed, 4 insertions(+) diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 626f822e..3b82b0dd 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -557,6 +557,7 @@ func (s *StackManager) copyDataExchangeConfigToVolumes() error { func (s *StackManager) createMember(id string, index int, options *types.InitOptions, external bool) (*types.Organization, error) { serviceBase := options.ServicesBasePort + (index * 100) + ptmBase := options.PtmBasePort + (index * 10) member := &types.Organization{ ID: id, Index: &index, @@ -565,6 +566,7 @@ func (s *StackManager) createMember(id string, index int, options *types.InitOpt ExposedConnectorPort: serviceBase + 2, ExposedUIPort: serviceBase + 3, ExposedDatabasePort: serviceBase + 4, + ExposePtmTpPort: ptmBase, External: external, OrgName: options.OrgNames[index], NodeName: options.NodeNames[index], @@ -783,6 +785,7 @@ func (s *StackManager) checkPortsAvailable() error { ports = append(ports, member.ExposedDatabasePort) ports = append(ports, member.ExposedUIPort) ports = append(ports, member.ExposedTokensPorts...) + ports = append(ports, member.ExposePtmTpPort) if !member.External { ports = append(ports, member.ExposedFireflyAdminSPIPort) diff --git a/pkg/types/organization.go b/pkg/types/organization.go index cbcd2469..4ecae854 100644 --- a/pkg/types/organization.go +++ b/pkg/types/organization.go @@ -32,6 +32,7 @@ type Organization struct { ExposedUIPort int `json:"exposedUiPort,omitempty"` ExposedSandboxPort int `json:"exposedSandboxPort,omitempty"` ExposedTokensPorts []int `json:"exposedTokensPorts,omitempty"` + ExposePtmTpPort int `json:"exposePtmTpPort,omitempty"` External bool `json:"external,omitempty"` OrgName string `json:"orgName,omitempty"` NodeName string `json:"nodeName,omitempty"` From 55a8385b61f384c838995cb5a94376c552234c22 Mon Sep 17 00:00:00 2001 From: rodion Date: Fri, 28 Jun 2024 06:57:55 +0000 Subject: [PATCH 15/22] Update tessera container name to have stack name Signed-off-by: rodion --- internal/blockchain/ethereum/quorum/geth_provider.go | 6 +++--- .../ethereum/quorum/private_transaction_manager.go | 4 ++-- internal/blockchain/ethereum/quorum/quorum.go | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/geth_provider.go index bcc52262..00bf4bad 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/geth_provider.go @@ -241,7 +241,7 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition ServiceName: fmt.Sprintf("tessera_%d", i), Service: &docker.Service{ Image: tesseraImage, - ContainerName: fmt.Sprintf("member%dtessera", i), + ContainerName: fmt.Sprintf("%s_member%dtessera", p.stack.Name, i), Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 @@ -347,14 +347,14 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %s", memberIndex)) tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) - if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount); err != nil { + if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount, p.stack.Name); err != nil { return nil, err } } l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %s", memberIndex)) quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) - if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { + if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), p.stack.Name, int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { return nil, err } diff --git a/internal/blockchain/ethereum/quorum/private_transaction_manager.go b/internal/blockchain/ethereum/quorum/private_transaction_manager.go index a5587375..eca97a1f 100644 --- a/internal/blockchain/ethereum/quorum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager.go @@ -76,12 +76,12 @@ func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name return privateKeyData.Data.Bytes, string(pubKeyBytes[:]), path, nil } -func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, memberCount string) error { +func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, memberCount, stackName string) error { // only tessera v09 onwards is supported var sb strings.Builder memberCountInt, _ := strconv.Atoi(memberCount) for i := 0; i < memberCountInt; i++ { - sb.WriteString(fmt.Sprintf("{\"url\":\"http://member%dtessera:%s\"},", i, TmP2pPort)) // construct peer list + sb.WriteString(fmt.Sprintf("{\"url\":\"http://%s_member%dtessera:%s\"},", stackName, i, TmP2pPort)) // construct peer list } peerList := strings.TrimSuffix(sb.String(), ",") content := fmt.Sprintf(`export JAVA_OPTS="-Xms128M -Xmx128M" diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index 0cbd623e..e77d4eed 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -25,7 +25,7 @@ import ( "github.com/hyperledger/firefly-cli/internal/docker" ) -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus string, chainId int, tesseraEnabled bool) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus, stackName string, chainId int, tesseraEnabled bool) error { discoveryCmd := "BOOTNODE_CMD=\"\"" connectTimeout := 15 if memberIndex != "0" { @@ -36,16 +36,16 @@ BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort, connectTimeout) tesseraCmd := "" if tesseraEnabled { - tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://member%stessera -TESSERA_TP_PORT=%s -TESSERA_Q2T_PORT=%s + tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://%[5]s_member%[1]stessera +TESSERA_TP_PORT=%[2]s +TESSERA_Q2T_PORT=%[3]s TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck ADDITIONAL_ARGS="${ADDITIONAL_ARGS:-} --ptm.timeout 5 --ptm.url ${TESSERA_URL}:${TESSERA_Q2T_PORT} --ptm.http.writebuffersize 4096 --ptm.http.readbuffersize 4096 --ptm.tls.mode off" echo -n "Checking tessera is up ... " -curl --connect-timeout %[4]d --max-time %[4]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "$TESSERA_UPCHECK_URL" +curl --connect-timeout %[4]d --max-time %[4]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --silent --fail "${TESSERA_UPCHECK_URL}" echo "" -`, memberIndex, TmTpPort, TmQ2tPort, connectTimeout) +`, memberIndex, TmTpPort, TmQ2tPort, connectTimeout, stackName) } content := fmt.Sprintf(`#!/bin/sh From c78b9260c2ade8020f75a067f414f357d017e2ff Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 1 Jul 2024 03:41:36 +0000 Subject: [PATCH 16/22] Fix PR review comments Signed-off-by: rodion --- cmd/init.go | 2 +- internal/blockchain/ethereum/ethereum.go | 2 +- internal/blockchain/ethereum/quorum/client.go | 8 +- .../blockchain/ethereum/quorum/genesis.go | 4 +- .../ethereum/quorum/genesis_test.go | 2 +- .../quorum/private_transaction_manager.go | 19 +- internal/blockchain/ethereum/quorum/quorum.go | 33 ++-- .../{geth_provider.go => quorum_provider.go} | 164 +++++++++--------- ...ovider_test.go => quorum_provider_test.go} | 46 ++--- internal/stacks/stack_manager.go | 4 +- 10 files changed, 146 insertions(+), 138 deletions(-) rename internal/blockchain/ethereum/quorum/{geth_provider.go => quorum_provider.go} (67%) rename internal/blockchain/ethereum/quorum/{geth_provider_test.go => quorum_provider_test.go} (91%) diff --git a/cmd/init.go b/cmd/init.go index 0e8e8dfb..e6084fcb 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -213,7 +213,7 @@ func validateQuorumConsensus(consensusString string) error { } if v != types.QuorumConsensusClique { - return errors.New("support for raft/ibft/qbft will come in future") + return errors.New("consensus algorithms such as raft/ibft/qbft for quorum not supported") } return nil diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 6951d6b5..4e8de22c 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -34,7 +34,7 @@ import ( type Account struct { Address string `json:"address"` PrivateKey string `json:"privateKey"` - PtmPublicKey string `json:"ptmPublicKey"` + PtmPublicKey string `json:"ptmPublicKey"` // Public key used for Tessera } func GenerateAddressAndPrivateKey() (address string, privateKey string) { diff --git a/internal/blockchain/ethereum/quorum/client.go b/internal/blockchain/ethereum/quorum/client.go index e4c8c761..27d9dc69 100644 --- a/internal/blockchain/ethereum/quorum/client.go +++ b/internal/blockchain/ethereum/quorum/client.go @@ -24,7 +24,7 @@ import ( "net/http" ) -type GethClient struct { +type QuorumClient struct { rpcURL string } @@ -47,13 +47,13 @@ type JSONRPCError struct { Message string `json:"message"` } -func NewGethClient(rpcURL string) *GethClient { - return &GethClient{ +func NewQuorumClient(rpcURL string) *QuorumClient { + return &QuorumClient{ rpcURL: rpcURL, } } -func (g *GethClient) UnlockAccount(address string, password string) error { +func (g *QuorumClient) UnlockAccount(address string, password string) error { requestBody, err := json.Marshal(&JSONRPCRequest{ JSONRPC: "2.0", ID: 0, diff --git a/internal/blockchain/ethereum/quorum/genesis.go b/internal/blockchain/ethereum/quorum/genesis.go index 46ef6bb7..265b10f2 100644 --- a/internal/blockchain/ethereum/quorum/genesis.go +++ b/internal/blockchain/ethereum/quorum/genesis.go @@ -63,7 +63,7 @@ type Alloc struct { func CreateGenesis(addresses []string, blockPeriod int, chainID int64) *Genesis { if blockPeriod == -1 { - blockPeriod = 0 + blockPeriod = 5 } extraData := "0x0000000000000000000000000000000000000000000000000000000000000000" alloc := make(map[string]*Alloc) @@ -92,7 +92,7 @@ func CreateGenesis(addresses []string, blockPeriod int, chainID int64) *Genesis }, }, Nonce: "0x0", - Timestamp: "0x60edb1c7", + Timestamp: "0x0", ExtraData: extraData, GasLimit: "0xffffff", Difficulty: "0x1", diff --git a/internal/blockchain/ethereum/quorum/genesis_test.go b/internal/blockchain/ethereum/quorum/genesis_test.go index 7e47856e..2a396113 100644 --- a/internal/blockchain/ethereum/quorum/genesis_test.go +++ b/internal/blockchain/ethereum/quorum/genesis_test.go @@ -75,7 +75,7 @@ func TestCreateGenesis(t *testing.T) { }, }, Nonce: "0x0", - Timestamp: "0x60edb1c7", + Timestamp: "0x0", ExtraData: extraData, GasLimit: "0xffffff", Difficulty: "0x1", diff --git a/internal/blockchain/ethereum/quorum/private_transaction_manager.go b/internal/blockchain/ethereum/quorum/private_transaction_manager.go index eca97a1f..97676f93 100644 --- a/internal/blockchain/ethereum/quorum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager.go @@ -22,17 +22,16 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "github.com/hyperledger/firefly-cli/internal/docker" ) -var entrypoint = "docker-entrypoint.sh" +var DockerEntrypoint = "docker-entrypoint.sh" var TmQ2tPort = "9101" var TmTpPort = "9080" var TmP2pPort = "9000" -var GethPort = "8545" +var QuorumPort = "8545" type PrivateKeyData struct { Bytes string `json:"bytes"` @@ -43,7 +42,7 @@ type PrivateKey struct { Data PrivateKeyData `json:"data"` } -func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name, password string) (privateKey, pubKey, path string, err error) { +func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name string) (privateKey, pubKey, path string, err error) { // generates both .pub and .key files used by Tessera var filename string if prefix != "" { @@ -73,14 +72,13 @@ func CreateTesseraKeys(ctx context.Context, image, outputDirectory, prefix, name if err != nil { return "", "", "", err } - return privateKeyData.Data.Bytes, string(pubKeyBytes[:]), path, nil + return privateKeyData.Data.Bytes, string(pubKeyBytes), path, nil } -func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, volumeName, memberCount, stackName string) error { +func CreateTesseraEntrypoint(ctx context.Context, outputDirectory, stackName string, memberCount int) error { // only tessera v09 onwards is supported var sb strings.Builder - memberCountInt, _ := strconv.Atoi(memberCount) - for i := 0; i < memberCountInt; i++ { + for i := 0; i < memberCount; i++ { sb.WriteString(fmt.Sprintf("{\"url\":\"http://%s_member%dtessera:%s\"},", stackName, i, TmP2pPort)) // construct peer list } peerList := strings.TrimSuffix(sb.String(), ",") @@ -144,7 +142,7 @@ cat < ${DDIR}/tessera-config-09.json EOF /tessera/bin/tessera -configfile ${DDIR}/tessera-config-09.json `, TmTpPort, TmQ2tPort, TmP2pPort, peerList) - filename := filepath.Join(outputDirectory, entrypoint) + filename := filepath.Join(outputDirectory, DockerEntrypoint) file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err @@ -154,7 +152,6 @@ EOF if err != nil { return err } - CopyTesseraEntrypointToVolume(ctx, outputDirectory, volumeName) return nil } @@ -162,7 +159,7 @@ func CopyTesseraEntrypointToVolume(ctx context.Context, tesseraEntrypointDirecto if err := docker.MkdirInVolume(ctx, volumeName, ""); err != nil { return err } - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraEntrypointDirectory, entrypoint), ""); err != nil { + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(tesseraEntrypointDirectory, DockerEntrypoint), ""); err != nil { return err } return nil diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index e77d4eed..8678b264 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -25,18 +25,18 @@ import ( "github.com/hyperledger/firefly-cli/internal/docker" ) -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, memberIndex, consensus, stackName string, chainId int, tesseraEnabled bool) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, consensus, stackName string, memberIndex, chainID, blockPeriodInSeconds int, tesseraEnabled bool) error { discoveryCmd := "BOOTNODE_CMD=\"\"" connectTimeout := 15 - if memberIndex != "0" { - discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://geth_0:%s -s --connect-timeout %[2]d --max-time %[2]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") + if memberIndex != 0 { + discoveryCmd = fmt.Sprintf(`bootnode=$(curl http://quorum_0:%s -s --connect-timeout %[2]d --max-time %[2]d --retry 5 --retry-connrefused --retry-delay 0 --retry-max-time 60 --fail --header "Content-Type: application/json" --data '{"jsonrpc":"2.0", "method": "admin_nodeInfo", "params": [], "id": 1}' | grep -o "enode://[a-z0-9@.:]*") BOOTNODE_CMD="--bootnodes $bootnode" -BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/geth_0}`, GethPort, connectTimeout) +BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/quorum_0}`, QuorumPort, connectTimeout) } tesseraCmd := "" if tesseraEnabled { - tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://%[5]s_member%[1]stessera + tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://%[5]s_member%[1]dtessera TESSERA_TP_PORT=%[2]s TESSERA_Q2T_PORT=%[3]s TESSERA_UPCHECK_URL=$TESSERA_URL:$TESSERA_TP_PORT/upcheck @@ -48,6 +48,12 @@ echo "" `, memberIndex, TmTpPort, TmQ2tPort, connectTimeout, stackName) } + blockPeriod := blockPeriodInSeconds + if blockPeriodInSeconds == -1 { + blockPeriod = 5 + } + blockPeriodInMs := blockPeriod * 60 + content := fmt.Sprintf(`#!/bin/sh set -o errexit @@ -55,11 +61,11 @@ set -o nounset set -o pipefail set -o xtrace -GOQUORUM_CONS_ALGO=%s +GOQUORUM_CONS_ALGO=%[1]s if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; then echo "Using istanbul for consensus algorithm..." - export CONSENSUS_ARGS="--istanbul.blockperiod 5 --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" + export CONSENSUS_ARGS="--istanbul.blockperiod %[6]d --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" export QUORUM_API="istanbul" elif [ "qbft" == "$GOQUORUM_CONS_ALGO" ]; then @@ -69,7 +75,7 @@ then elif [ "raft" == "$GOQUORUM_CONS_ALGO" ]; then echo "Using raft for consensus algorithm..." - export CONSENSUS_ARGS="--raft --raftblocktime 300 --raftport 53000" + export CONSENSUS_ARGS="--raft --raftblocktime %[7]d --raftport 53000" export QUORUM_API="raft" elif [ "clique" == "$GOQUORUM_CONS_ALGO" ]; then @@ -79,15 +85,15 @@ then fi ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} -%s +%[2]s # discovery -%s +%[3]s echo "bootnode discovery command :: $BOOTNODE_CMD" IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, GethPort, chainId) - filename := filepath.Join(outputDirectory, entrypoint) +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %[4]s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %[5]d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, QuorumPort, chainID, blockPeriod, blockPeriodInMs) + filename := filepath.Join(outputDirectory, DockerEntrypoint) if err := os.MkdirAll(outputDirectory, 0755); err != nil { return err } @@ -100,12 +106,11 @@ exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason if err != nil { return err } - CopyQuorumEntrypointToVolume(ctx, outputDirectory, volumeName) return nil } func CopyQuorumEntrypointToVolume(ctx context.Context, quorumEntrypointDirectory, volumeName string) error { - if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(quorumEntrypointDirectory, entrypoint), ""); err != nil { + if err := docker.CopyFileToVolume(ctx, volumeName, filepath.Join(quorumEntrypointDirectory, DockerEntrypoint), ""); err != nil { return err } return nil diff --git a/internal/blockchain/ethereum/quorum/geth_provider.go b/internal/blockchain/ethereum/quorum/quorum_provider.go similarity index 67% rename from internal/blockchain/ethereum/quorum/geth_provider.go rename to internal/blockchain/ethereum/quorum/quorum_provider.go index 00bf4bad..6df85af2 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider.go @@ -36,20 +36,20 @@ import ( "github.com/hyperledger/firefly-cli/pkg/types" ) -var gethImage = "quorumengineering/quorum:24.4" +var quorumImage = "quorumengineering/quorum:24.4" var tesseraImage = "quorumengineering/tessera:24.4" var exposedBlockchainPortMultiplier = 10 // TODO: Probably randomize this and make it different per member? var keyPassword = "correcthorsebatterystaple" -type GethProvider struct { +type QuorumProvider struct { ctx context.Context stack *types.Stack connector connector.Connector } -func NewGethProvider(ctx context.Context, stack *types.Stack) *GethProvider { +func NewQuorumProvider(ctx context.Context, stack *types.Stack) *QuorumProvider { var connector connector.Connector switch stack.BlockchainConnector { case types.BlockchainConnectorEthconnect: @@ -58,28 +58,44 @@ func NewGethProvider(ctx context.Context, stack *types.Stack) *GethProvider { connector = evmconnect.NewEvmconnect(ctx) } - return &GethProvider{ + return &QuorumProvider{ ctx: ctx, stack: stack, connector: connector, } } -func (p *GethProvider) WriteConfig(options *types.InitOptions) error { +func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { + l := log.LoggerFromContext(p.ctx) initDir := filepath.Join(constants.StacksDir, p.stack.Name, "init") for i, member := range p.stack.Members { // Generate the connector config for each member connectorConfigPath := filepath.Join(initDir, "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) - if err := p.connector.GenerateConfig(p.stack, member, fmt.Sprintf("geth_%d", i)).WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { + if err := p.connector.GenerateConfig(p.stack, member, fmt.Sprintf("quorum_%d", i)).WriteConfig(connectorConfigPath, options.ExtraConnectorConfigPath); err != nil { return nil } + + // Generate tessera docker-entrypoint for each member + l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %d", i)) + tesseraEntrypointOutputDirectory := filepath.Join(initDir, "tessera", fmt.Sprintf("tessera_%d", i)) + if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, p.stack.Name, len(p.stack.Members)); err != nil { + return err + } + + // Generate quorum docker-entrypoint for each member + l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %d", i)) + quorumEntrypointOutputDirectory := filepath.Join(initDir, "blockchain", fmt.Sprintf("quorum_%d", i)) + quorumVolumeName := fmt.Sprintf("%s_quorum_%d", p.stack.Name, i) + if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, quorumVolumeName, p.stack.QuorumConsensus.String(), p.stack.Name, i, int(p.stack.ChainID()), options.BlockPeriod, p.stack.TesseraEnabled); err != nil { + return err + } } // Create genesis.json addresses := make([]string, len(p.stack.Members)) for i, member := range p.stack.Members { address := member.Account.(*ethereum.Account).Address - // Drop the 0x on the front of the address here because that's what geth is expecting in the genesis.json + // Drop the 0x on the front of the address here because that's what quorum is expecting in the genesis.json addresses[i] = address[2:] } genesis := CreateGenesis(addresses, options.BlockPeriod, p.stack.ChainID()) @@ -90,13 +106,13 @@ func (p *GethProvider) WriteConfig(options *types.InitOptions) error { return nil } -func (p *GethProvider) FirstTimeSetup() error { - gethVolumeName := fmt.Sprintf("%s_geth", p.stack.Name) +func (p *QuorumProvider) FirstTimeSetup() error { + quorumVolumeName := fmt.Sprintf("%s_quorum", p.stack.Name) tesseraVolumeName := fmt.Sprintf("%s_tessera", p.stack.Name) blockchainDir := path.Join(p.stack.RuntimeDir, "blockchain") tesseraDir := path.Join(p.stack.RuntimeDir, "tessera") - tesseraDirWithinContainer := "/" contractsDir := path.Join(p.stack.RuntimeDir, "contracts") + rootDir := "/" if err := p.connector.FirstTimeSetup(p.stack); err != nil { return err @@ -113,49 +129,58 @@ func (p *GethProvider) FirstTimeSetup() error { if err := docker.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { return err } - } - for i := range p.stack.Members { - gethVolumeNameMember := fmt.Sprintf("%s_%d", gethVolumeName, i) + // Volume name instantiation + quorumVolumeNameMember := fmt.Sprintf("%s_%d", quorumVolumeName, i) tesseraVolumeNameMember := fmt.Sprintf("%s_%d", tesseraVolumeName, i) // Copy the wallet files of each member to their respective blockchain volume - keystoreDirectory := filepath.Join(blockchainDir, fmt.Sprintf("geth_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, keystoreDirectory, "/"); err != nil { + keystoreDirectory := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), "keystore") + if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, keystoreDirectory, "/"); err != nil { return err } if p.stack.TesseraEnabled { - // Copy member specific tessera key files to each of the tessera volume - if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, tesseraDirWithinContainer); err != nil { + // Copy member specific tessera key files + if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { + return err + } + tmKeystoreDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmKeystoreDirectory, rootDir); err != nil { return err } - tmDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmDirectory, tesseraDirWithinContainer); err != nil { + // Copy tessera docker-entrypoint file + tmEntrypointPath := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), DockerEntrypoint) + if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmEntrypointPath, rootDir); err != nil { return err } } + // Copy quorum docker-entrypoint file + quorumEntrypointPath := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), DockerEntrypoint) + if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, quorumEntrypointPath, rootDir); err != nil { + return err + } + // Copy the genesis block information - if err := docker.CopyFileToVolume(p.ctx, gethVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { + if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { return err } // Initialize the genesis block - if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", gethVolumeNameMember), gethImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { + if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", quorumVolumeNameMember), quorumImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { return err } - } return nil } -func (p *GethProvider) PreStart() error { +func (p *QuorumProvider) PreStart() error { return nil } -func (p *GethProvider) PostStart(firstTimeSetup bool) error { +func (p *QuorumProvider) PostStart(firstTimeSetup bool) error { l := log.LoggerFromContext(p.ctx) // Unlock accounts for _, account := range p.stack.State.Accounts { @@ -176,14 +201,14 @@ func (p *GethProvider) PostStart(firstTimeSetup bool) error { return nil } -func (p *GethProvider) unlockAccount(address, password string, memberIndex int) error { +func (p *QuorumProvider) unlockAccount(address, password string, memberIndex int) error { l := log.LoggerFromContext(p.ctx) verbose := log.VerbosityFromContext(p.ctx) // exposed blockchain port is the default for node 0, we need to add the port multiplier to get the right rpc for the correct node - gethClient := NewGethClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*exposedBlockchainPortMultiplier))) + quorumClient := NewQuorumClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*exposedBlockchainPortMultiplier))) retries := 10 for { - if err := gethClient.UnlockAccount(address, password); err != nil { + if err := quorumClient.UnlockAccount(address, password); err != nil { if verbose { l.Debug(err.Error()) } @@ -199,7 +224,7 @@ func (p *GethProvider) unlockAccount(address, password string, memberIndex int) return nil } -func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, error) { +func (p *QuorumProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, error) { contract, err := ethereum.ReadFireFlyContract(p.ctx, p.stack) if err != nil { return nil, err @@ -207,7 +232,7 @@ func (p *GethProvider) DeployFireFlyContract() (*types.ContractDeploymentResult, return p.connector.DeployContract(contract, "FireFly", p.stack.Members[0], nil) } -func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { +func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { memberCount := len(p.stack.Members) serviceDefinitionsCount := memberCount if p.stack.TesseraEnabled { @@ -216,27 +241,9 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition serviceDefinitions := make([]*docker.ServiceDefinition, serviceDefinitionsCount) connectorDependents := map[string]string{} for i := 0; i < memberCount; i++ { - var dependsOn map[string]map[string]string - if p.stack.TesseraEnabled { - dependsOn = map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}} - } - serviceDefinitions[i] = &docker.ServiceDefinition{ - ServiceName: fmt.Sprintf("geth_%d", i), - Service: &docker.Service{ - Image: gethImage, - ContainerName: fmt.Sprintf("%s_geth_%d", p.stack.Name, i), - Volumes: []string{fmt.Sprintf("geth_%d:/data", i)}, - Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 - Environment: p.stack.EnvironmentVars, - EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, - DependsOn: dependsOn, - }, - VolumeNames: []string{fmt.Sprintf("geth_%d", i)}, - } - connectorDependents[fmt.Sprintf("geth_%d", i)] = "service_started" - + var quorumDependsOn map[string]map[string]string if p.stack.TesseraEnabled { + quorumDependsOn = map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}} serviceDefinitions[i+memberCount] = &docker.ServiceDefinition{ ServiceName: fmt.Sprintf("tessera_%d", i), Service: &docker.Service{ @@ -252,12 +259,27 @@ func (p *GethProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition VolumeNames: []string{fmt.Sprintf("tessera_%d", i)}, } } + serviceDefinitions[i] = &docker.ServiceDefinition{ + ServiceName: fmt.Sprintf("quorum_%d", i), + Service: &docker.Service{ + Image: quorumImage, + ContainerName: fmt.Sprintf("%s_quorum_%d", p.stack.Name, i), + Volumes: []string{fmt.Sprintf("quorum_%d:/data", i)}, + Logging: docker.StandardLogOptions, + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 + Environment: p.stack.EnvironmentVars, + EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, + DependsOn: quorumDependsOn, + }, + VolumeNames: []string{fmt.Sprintf("quorum_%d", i)}, + } + connectorDependents[fmt.Sprintf("quorum_%d", i)] = "service_started" } serviceDefinitions = append(serviceDefinitions, p.connector.GetServiceDefinitions(p.stack, connectorDependents)...) return serviceDefinitions } -func (p *GethProvider) GetBlockchainPluginConfig(stack *types.Stack, m *types.Organization) (blockchainConfig *types.BlockchainConfig) { +func (p *QuorumProvider) GetBlockchainPluginConfig(stack *types.Stack, m *types.Organization) (blockchainConfig *types.BlockchainConfig) { var connectorURL string if m.External { connectorURL = p.GetConnectorExternalURL(m) @@ -277,7 +299,7 @@ func (p *GethProvider) GetBlockchainPluginConfig(stack *types.Stack, m *types.Or return } -func (p *GethProvider) GetOrgConfig(stack *types.Stack, m *types.Organization) (orgConfig *types.OrgConfig) { +func (p *QuorumProvider) GetOrgConfig(stack *types.Stack, m *types.Organization) (orgConfig *types.OrgConfig) { account := m.Account.(*ethereum.Account) orgConfig = &types.OrgConfig{ Name: m.OrgName, @@ -286,11 +308,11 @@ func (p *GethProvider) GetOrgConfig(stack *types.Stack, m *types.Organization) ( return } -func (p *GethProvider) Reset() error { +func (p *QuorumProvider) Reset() error { return nil } -func (p *GethProvider) GetContracts(filename string, extraArgs []string) ([]string, error) { +func (p *QuorumProvider) GetContracts(filename string, extraArgs []string) ([]string, error) { contracts, err := ethereum.ReadContractJSON(filename) if err != nil { return []string{}, err @@ -304,7 +326,7 @@ func (p *GethProvider) GetContracts(filename string, extraArgs []string) ([]stri return contractNames, err } -func (p *GethProvider) DeployContract(filename, contractName, instanceName string, member *types.Organization, extraArgs []string) (*types.ContractDeploymentResult, error) { +func (p *QuorumProvider) DeployContract(filename, contractName, instanceName string, member *types.Organization, extraArgs []string) (*types.ContractDeploymentResult, error) { contracts, err := ethereum.ReadContractJSON(filename) if err != nil { return nil, err @@ -312,11 +334,10 @@ func (p *GethProvider) DeployContract(filename, contractName, instanceName strin return p.connector.DeployContract(contracts.Contracts[contractName], instanceName, member, extraArgs) } -func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { +func (p *QuorumProvider) CreateAccount(args []string) (interface{}, error) { l := log.LoggerFromContext(p.ctx) memberIndex := args[2] - memberCount := args[3] - gethVolumeName := fmt.Sprintf("%s_geth_%s", p.stack.Name, memberIndex) + quorumVolumeName := fmt.Sprintf("%s_quorum_%s", p.stack.Name, memberIndex) var directory string stackHasRunBefore, err := p.stack.HasRunBefore() if err != nil { @@ -329,7 +350,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { } prefix := strconv.FormatInt(time.Now().UnixNano(), 10) - outputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex), "keystore") + outputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("quorum_%s", memberIndex), "keystore") keyPair, walletFilePath, err := ethereum.CreateWalletFile(outputDirectory, prefix, keyPassword) if err != nil { return nil, err @@ -338,28 +359,16 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { // Tessera is an optional add-on to the quorum blockchain node provider var tesseraPubKey, tesseraKeysPath string if p.stack.TesseraEnabled { - tesseraVolumeName := fmt.Sprintf("%s_tessera_%s", p.stack.Name, memberIndex) tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") - _, tesseraPubKey, tesseraKeysPath, err = CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm", keyPassword) + _, tesseraPubKey, tesseraKeysPath, err = CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm") if err != nil { return nil, err } l.Info(fmt.Sprintf("keys generated in %s", tesseraKeysPath)) - l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %s", memberIndex)) - tesseraEntrypointOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex)) - if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, tesseraVolumeName, memberCount, p.stack.Name); err != nil { - return nil, err - } - } - - l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %s", memberIndex)) - quorumEntrypointOutputDirectory := filepath.Join(directory, "blockchain", fmt.Sprintf("geth_%s", memberIndex)) - if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName, memberIndex, p.stack.QuorumConsensus.String(), p.stack.Name, int(p.stack.ChainID()), p.stack.TesseraEnabled); err != nil { - return nil, err } if stackHasRunBefore { - if err := ethereum.CopyWalletFileToVolume(p.ctx, walletFilePath, gethVolumeName); err != nil { + if err := ethereum.CopyWalletFileToVolume(p.ctx, walletFilePath, quorumVolumeName); err != nil { return nil, err } if memberIndexInt, err := strconv.Atoi(memberIndex); err != nil { @@ -369,9 +378,6 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { return nil, err } } - if err := CopyQuorumEntrypointToVolume(p.ctx, quorumEntrypointOutputDirectory, gethVolumeName); err != nil { - return nil, err - } } return ðereum.Account{ @@ -381,7 +387,7 @@ func (p *GethProvider) CreateAccount(args []string) (interface{}, error) { }, nil } -func (p *GethProvider) ParseAccount(account interface{}) interface{} { +func (p *QuorumProvider) ParseAccount(account interface{}) interface{} { accountMap := account.(map[string]interface{}) ptmPublicKey := "" // if we start quorum without tessera, no public key will be generated v, ok := accountMap["ptmPublicKey"] @@ -396,14 +402,14 @@ func (p *GethProvider) ParseAccount(account interface{}) interface{} { } } -func (p *GethProvider) GetConnectorName() string { +func (p *QuorumProvider) GetConnectorName() string { return p.connector.Name() } -func (p *GethProvider) GetConnectorURL(org *types.Organization) string { +func (p *QuorumProvider) GetConnectorURL(org *types.Organization) string { return fmt.Sprintf("http://%s_%s:%v", p.connector.Name(), org.ID, p.connector.Port()) } -func (p *GethProvider) GetConnectorExternalURL(org *types.Organization) string { +func (p *QuorumProvider) GetConnectorExternalURL(org *types.Organization) string { return fmt.Sprintf("http://127.0.0.1:%v", org.ExposedConnectorPort) } diff --git a/internal/blockchain/ethereum/quorum/geth_provider_test.go b/internal/blockchain/ethereum/quorum/quorum_provider_test.go similarity index 91% rename from internal/blockchain/ethereum/quorum/geth_provider_test.go rename to internal/blockchain/ethereum/quorum/quorum_provider_test.go index 136d151d..34282829 100644 --- a/internal/blockchain/ethereum/quorum/geth_provider_test.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewGethProvider(t *testing.T) { +func TestNewQuorumProvider(t *testing.T) { var ctx context.Context testcases := []struct { @@ -27,11 +27,11 @@ func TestNewGethProvider(t *testing.T) { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ - Name: "TestGethWithEvmConnect", + Name: "TestQuorumWithEvmConnect", Members: []*types.Organization{ { OrgName: "Org1", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1234567890abcdef0123456789abcdef6789abcd", PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", @@ -39,7 +39,7 @@ func TestNewGethProvider(t *testing.T) { }, { OrgName: "Org2", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1234567890abcdef012345670000000000000000", PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", @@ -55,11 +55,11 @@ func TestNewGethProvider(t *testing.T) { Name: "testcase2", Ctx: ctx, Stack: &types.Stack{ - Name: "TestGethWithEthConnect", + Name: "TestQuorumWithEthConnect", Members: []*types.Organization{ { OrgName: "Org55", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1f2a000000000000000000000000000000000000", PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", @@ -74,8 +74,8 @@ func TestNewGethProvider(t *testing.T) { } for _, tc := range testcases { t.Run(tc.Name, func(t *testing.T) { - gethProvider := NewGethProvider(tc.Ctx, tc.Stack) - assert.NotNil(t, gethProvider) + quorumProvider := NewQuorumProvider(tc.Ctx, tc.Stack) + assert.NotNil(t, quorumProvider) }) } } @@ -126,8 +126,8 @@ func TestParseAccount(t *testing.T) { } for _, tc := range testcases { t.Run(tc.Name, func(t *testing.T) { - gethProvider := &GethProvider{} - result := gethProvider.ParseAccount(tc.Address) + quorumProvider := &QuorumProvider{} + result := quorumProvider.ParseAccount(tc.Address) _, ok := result.(*ethereum.Account) if !ok { @@ -152,7 +152,7 @@ func TestGetOrgConfig(t *testing.T) { }, Org: &types.Organization{ OrgName: "Org-1", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1234567890abcdef0123456789abcdef6789abcd", PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", @@ -170,7 +170,7 @@ func TestGetOrgConfig(t *testing.T) { }, Org: &types.Organization{ OrgName: "Org-2", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1f2a000000000000000000000000000000000000", PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", @@ -188,7 +188,7 @@ func TestGetOrgConfig(t *testing.T) { }, Org: &types.Organization{ OrgName: "Org-3", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0xabcdeffedcba9876543210abcdeffedc00000000", PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", @@ -202,7 +202,7 @@ func TestGetOrgConfig(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - p := &GethProvider{} + p := &QuorumProvider{} Orgconfig := p.GetOrgConfig(tc.Stack, tc.Org) assert.NotNil(t, Orgconfig) @@ -229,7 +229,7 @@ func TestGetContracts(t *testing.T) { } } }` - p := &GethProvider{} + p := &QuorumProvider{} err := os.WriteFile(testContractFile, []byte(testContractJSON), 0755) if err != nil { @@ -253,7 +253,7 @@ func TestGetConnectorExternal(t *testing.T) { Name: "testcase1", Org: &types.Organization{ OrgName: "Org-1", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0x1f2a000000000000000000000000000000000000", PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", @@ -266,7 +266,7 @@ func TestGetConnectorExternal(t *testing.T) { Name: "testcase2", Org: &types.Organization{ OrgName: "Org-2", - NodeName: "geth", + NodeName: "quorum", Account: ðereum.Account{ Address: "0xabcdeffedcba9876543210abcdeffedc00000000", PrivateKey: "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff", @@ -277,7 +277,7 @@ func TestGetConnectorExternal(t *testing.T) { }, } for _, tc := range testcase { - p := &GethProvider{} + p := &QuorumProvider{} result := p.GetConnectorExternalURL(tc.Org) assert.Equal(t, tc.ExpectedPort, result) } @@ -295,32 +295,32 @@ func TestCreateAccount(t *testing.T) { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_geth", + Name: "Org-1_quorum", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), }, - Args: []string{"Org-1_geth", "Org-1_geth", "0", "1"}, + Args: []string{"Org-1_quorum", "Org-1_quorum", "0", "1"}, }, { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-2_geth", + Name: "Org-2_quorum", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), }, - Args: []string{"Org-2_geth", "Org-2_geth", "1", "2"}, + Args: []string{"Org-2_quorum", "Org-2_quorum", "1", "2"}, }, } for _, tc := range testcases { t.Run(tc.Name, func(t *testing.T) { - p := NewGethProvider(tc.Ctx, tc.Stack) + p := NewQuorumProvider(tc.Ctx, tc.Stack) Account, err := p.CreateAccount(tc.Args) if err != nil { t.Log("unable to create account", err) diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 3b82b0dd..54faff79 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -591,7 +591,7 @@ func (s *StackManager) createMember(id string, index int, options *types.InitOpt nextPort++ } - account, err := s.blockchainProvider.CreateAccount([]string{member.OrgName, member.OrgName, strconv.Itoa(index), strconv.Itoa(options.MemberCount)}) + account, err := s.blockchainProvider.CreateAccount([]string{member.OrgName, member.OrgName, strconv.Itoa(index)}) if err != nil { return nil, err } @@ -1334,7 +1334,7 @@ func (s *StackManager) getBlockchainProvider() blockchain.IBlockchainProvider { case types.BlockchainNodeProviderBesu: return besu.NewBesuProvider(s.ctx, s.Stack) case types.BlockchainNodeProviderQuorum: - return quorum.NewGethProvider(s.ctx, s.Stack) + return quorum.NewQuorumProvider(s.ctx, s.Stack) case types.BlockchainNodeProviderRemoteRPC: s.Stack.DisableTokenFactories = true return ethremoterpc.NewRemoteRPCProvider(s.ctx, s.Stack) From 3af75a792a7700b3ad07fbf8798a8956b39cae3a Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 1 Jul 2024 09:34:26 +0000 Subject: [PATCH 17/22] Remove unused volume parameter when creating quorum entrypoint Signed-off-by: rodion --- internal/blockchain/ethereum/quorum/quorum.go | 2 +- internal/blockchain/ethereum/quorum/quorum_provider.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index 8678b264..b6aa2294 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -25,7 +25,7 @@ import ( "github.com/hyperledger/firefly-cli/internal/docker" ) -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, volumeName, consensus, stackName string, memberIndex, chainID, blockPeriodInSeconds int, tesseraEnabled bool) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, consensus, stackName string, memberIndex, chainID, blockPeriodInSeconds int, tesseraEnabled bool) error { discoveryCmd := "BOOTNODE_CMD=\"\"" connectTimeout := 15 if memberIndex != 0 { diff --git a/internal/blockchain/ethereum/quorum/quorum_provider.go b/internal/blockchain/ethereum/quorum/quorum_provider.go index 6df85af2..fb8b701e 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider.go @@ -85,8 +85,7 @@ func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { // Generate quorum docker-entrypoint for each member l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %d", i)) quorumEntrypointOutputDirectory := filepath.Join(initDir, "blockchain", fmt.Sprintf("quorum_%d", i)) - quorumVolumeName := fmt.Sprintf("%s_quorum_%d", p.stack.Name, i) - if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, quorumVolumeName, p.stack.QuorumConsensus.String(), p.stack.Name, i, int(p.stack.ChainID()), options.BlockPeriod, p.stack.TesseraEnabled); err != nil { + if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, p.stack.QuorumConsensus.String(), p.stack.Name, i, int(p.stack.ChainID()), options.BlockPeriod, p.stack.TesseraEnabled); err != nil { return err } } From 19cb4187065533d26fa5013d21a0343eb44ae0a1 Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 1 Jul 2024 09:49:00 +0000 Subject: [PATCH 18/22] Fix incorrect consensus args Signed-off-by: rodion --- internal/blockchain/ethereum/quorum/quorum.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index b6aa2294..9bb2bcb6 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -62,7 +62,7 @@ set -o pipefail set -o xtrace GOQUORUM_CONS_ALGO=%[1]s -if [ "istanbul" == "$GOQUORUM_CONS_ALGO" ]; +if [ "ibft" == "$GOQUORUM_CONS_ALGO" ]; then echo "Using istanbul for consensus algorithm..." export CONSENSUS_ARGS="--istanbul.blockperiod %[6]d --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints" From 2a12af01aecefd4d9b14fb7bbd748244adc775d4 Mon Sep 17 00:00:00 2001 From: rodion Date: Mon, 1 Jul 2024 09:57:13 +0000 Subject: [PATCH 19/22] Add tests for quorum package Signed-off-by: rodion --- .../ethereum/connector/ethconnect/config.go | 5 + .../ethereum/connector/evmconnect/config.go | 5 + .../blockchain/ethereum/quorum/client_test.go | 81 +++++ .../blockchain/ethereum/quorum/genesis.go | 5 + .../quorum/private_transaction_manager.go | 3 + .../private_transaction_manager_test.go | 112 +++++++ .../ethereum/quorum/quorum_provider.go | 40 ++- .../ethereum/quorum/quorum_provider_test.go | 313 ++++++++++++++++-- .../blockchain/ethereum/quorum/quorum_test.go | 72 ++++ internal/docker/docker_manager.go | 104 ++++++ internal/docker/mocks/docker_manager.go | 62 ++++ 11 files changed, 759 insertions(+), 43 deletions(-) create mode 100644 internal/blockchain/ethereum/quorum/client_test.go create mode 100644 internal/blockchain/ethereum/quorum/private_transaction_manager_test.go create mode 100644 internal/blockchain/ethereum/quorum/quorum_test.go create mode 100644 internal/docker/docker_manager.go create mode 100644 internal/docker/mocks/docker_manager.go diff --git a/internal/blockchain/ethereum/connector/ethconnect/config.go b/internal/blockchain/ethereum/connector/ethconnect/config.go index f15f3444..03df319c 100644 --- a/internal/blockchain/ethereum/connector/ethconnect/config.go +++ b/internal/blockchain/ethereum/connector/ethconnect/config.go @@ -19,6 +19,7 @@ package ethconnect import ( "fmt" "os" + "path/filepath" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector" "github.com/hyperledger/firefly-cli/pkg/types" @@ -58,6 +59,10 @@ type HTTP struct { func (e *Config) WriteConfig(filename string, extraConnectorConfigPath string) error { configYamlBytes, _ := yaml.Marshal(e) + basedir := filepath.Dir(filename) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } if err := os.WriteFile(filename, configYamlBytes, 0755); err != nil { return err } diff --git a/internal/blockchain/ethereum/connector/evmconnect/config.go b/internal/blockchain/ethereum/connector/evmconnect/config.go index 3b5beab0..877a9c23 100644 --- a/internal/blockchain/ethereum/connector/evmconnect/config.go +++ b/internal/blockchain/ethereum/connector/evmconnect/config.go @@ -19,6 +19,7 @@ package evmconnect import ( "fmt" "os" + "path/filepath" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector" "github.com/hyperledger/firefly-cli/pkg/types" @@ -75,6 +76,10 @@ type GasOracleConfig struct { func (e *Config) WriteConfig(filename string, extraEvmconnectConfigPath string) error { configYamlBytes, _ := yaml.Marshal(e) + basedir := filepath.Dir(filename) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } if err := os.WriteFile(filename, configYamlBytes, 0755); err != nil { return err } diff --git a/internal/blockchain/ethereum/quorum/client_test.go b/internal/blockchain/ethereum/quorum/client_test.go new file mode 100644 index 00000000..11e42ce8 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/client_test.go @@ -0,0 +1,81 @@ +package quorum + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-cli/internal/utils" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestUnlockAccount(t *testing.T) { + tests := []struct { + Name string + RPCUrl string + Address string + Password string + StatusCode int + ApiResponse *JSONRPCResponse + }{ + { + Name: "TestUnlockAccount-1", + RPCUrl: "http://127.0.0.1:8545", + Address: "user-1", + Password: "POST", + StatusCode: 200, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: nil, + Result: "mock result", + }, + }, + { + Name: "TestUnlockAccountError-2", + RPCUrl: "http://127.0.0.1:8545", + Address: "user-1", + Password: "POST", + StatusCode: 200, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: &JSONRPCError{500, "invalid account"}, + Result: "mock result", + }, + }, + { + Name: "TestUnlockAccountHTTPError-3", + RPCUrl: "http://localhost:8545", + Address: "user-1", + Password: "POST", + StatusCode: 500, + ApiResponse: &JSONRPCResponse{ + JSONRPC: "2.0", + ID: 0, + Error: nil, + Result: "mock result", + }, + }, + } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + apiResponse, _ := json.Marshal(tc.ApiResponse) + // mockResponse + httpmock.RegisterResponder("POST", tc.RPCUrl, + httpmock.NewStringResponder(tc.StatusCode, string(apiResponse))) + client := NewQuorumClient(tc.RPCUrl) + utils.StartMockServer(t) + err := client.UnlockAccount(tc.Address, tc.Password) + utils.StopMockServer(t) + + // expect errors when returned status code != 200 or ApiResponse comes back with non nil error + if tc.StatusCode != 200 || tc.ApiResponse.Error != nil { + assert.NotNil(t, err, "expects error to be returned when either quorum returns an application error or non 200 http response") + } else { + assert.NoError(t, err, fmt.Sprintf("unable to unlock account: %v", err)) + } + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/genesis.go b/internal/blockchain/ethereum/quorum/genesis.go index 265b10f2..e9751dd1 100644 --- a/internal/blockchain/ethereum/quorum/genesis.go +++ b/internal/blockchain/ethereum/quorum/genesis.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" ) @@ -107,6 +108,10 @@ func CreateGenesis(addresses []string, blockPeriod int, chainID int64) *Genesis func (g *Genesis) WriteGenesisJSON(filename string) error { genesisJSONBytes, _ := json.MarshalIndent(g, "", " ") + basedir := filepath.Dir(filename) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } if err := os.WriteFile(filename, genesisJSONBytes, 0755); err != nil { return err } diff --git a/internal/blockchain/ethereum/quorum/private_transaction_manager.go b/internal/blockchain/ethereum/quorum/private_transaction_manager.go index 97676f93..5e40bdb2 100644 --- a/internal/blockchain/ethereum/quorum/private_transaction_manager.go +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager.go @@ -143,6 +143,9 @@ EOF /tessera/bin/tessera -configfile ${DDIR}/tessera-config-09.json `, TmTpPort, TmQ2tPort, TmP2pPort, peerList) filename := filepath.Join(outputDirectory, DockerEntrypoint) + if err := os.MkdirAll(outputDirectory, 0755); err != nil { + return err + } file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err diff --git a/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go b/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go new file mode 100644 index 00000000..938f7604 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/private_transaction_manager_test.go @@ -0,0 +1,112 @@ +package quorum + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestCreateTesseraKeys(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + TesseraImage string + KeysPrefix string + KeysName string + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + TesseraImage: "quorumengineering/tessera:24.4", + KeysPrefix: "", + KeysName: "tm", + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + TesseraImage: "quorumengineering/tessera:24.4", + KeysPrefix: "xyz", + KeysName: "tm", + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + privateKey, publicKey, tesseraKeysPath, err := CreateTesseraKeys(ctx, tc.TesseraImage, filepath.Join(tc.Stack.InitDir, "tessera", "tessera_0", "keystore"), tc.KeysPrefix, tc.KeysName) + if err != nil { + t.Log("unable to create tessera keys", err) + } + //validate properties of tessera keys + assert.NotEmpty(t, privateKey) + assert.NotEmpty(t, publicKey) + assert.NotEmpty(t, tesseraKeysPath) + + expectedOutputName := tc.KeysName + if tc.KeysPrefix != "" { + expectedOutputName = fmt.Sprintf("%s_%s", tc.KeysPrefix, expectedOutputName) + } + assert.Equal(t, tesseraKeysPath, filepath.Join(tc.Stack.InitDir, "tessera", "tessera_0", "keystore", expectedOutputName), "invalid output path") + + assert.Nil(t, err) + }) + } +} + +func TestCreateTesseraEntrypoint(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + StackName string + MemberCount int + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + StackName: "org1", + MemberCount: 4, + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-2_quorum", + InitDir: t.TempDir(), + }, + StackName: "org2", + MemberCount: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + err := CreateTesseraEntrypoint(ctx, tc.Stack.InitDir, tc.StackName, tc.MemberCount) + if err != nil { + t.Log("unable to create tessera docker entrypoint", err) + } + path := filepath.Join(tc.Stack.InitDir, "docker-entrypoint.sh") + _, err = os.Stat(path) + assert.NoError(t, err, "docker entrypoint file not created") + + b, err := os.ReadFile(path) + assert.NoError(t, err, "unable to read docker entrypoint file") + for i := 0; i < tc.MemberCount; i++ { + strings.Contains(string(b), fmt.Sprintf("member%dtessera", i)) + } + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/quorum_provider.go b/internal/blockchain/ethereum/quorum/quorum_provider.go index fb8b701e..efe541cb 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider.go @@ -30,7 +30,6 @@ import ( "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector/ethconnect" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum/connector/evmconnect" - "github.com/hyperledger/firefly-cli/internal/constants" "github.com/hyperledger/firefly-cli/internal/docker" "github.com/hyperledger/firefly-cli/internal/log" "github.com/hyperledger/firefly-cli/pkg/types" @@ -38,7 +37,7 @@ import ( var quorumImage = "quorumengineering/quorum:24.4" var tesseraImage = "quorumengineering/tessera:24.4" -var exposedBlockchainPortMultiplier = 10 +var ExposedBlockchainPortMultiplier = 10 // TODO: Probably randomize this and make it different per member? var keyPassword = "correcthorsebatterystaple" @@ -47,6 +46,7 @@ type QuorumProvider struct { ctx context.Context stack *types.Stack connector connector.Connector + dockerMgr docker.IDockerManager } func NewQuorumProvider(ctx context.Context, stack *types.Stack) *QuorumProvider { @@ -62,12 +62,13 @@ func NewQuorumProvider(ctx context.Context, stack *types.Stack) *QuorumProvider ctx: ctx, stack: stack, connector: connector, + dockerMgr: docker.NewDockerManager(), } } func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { l := log.LoggerFromContext(p.ctx) - initDir := filepath.Join(constants.StacksDir, p.stack.Name, "init") + initDir := p.stack.InitDir for i, member := range p.stack.Members { // Generate the connector config for each member connectorConfigPath := filepath.Join(initDir, "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) @@ -76,10 +77,12 @@ func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { } // Generate tessera docker-entrypoint for each member - l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %d", i)) - tesseraEntrypointOutputDirectory := filepath.Join(initDir, "tessera", fmt.Sprintf("tessera_%d", i)) - if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, p.stack.Name, len(p.stack.Members)); err != nil { - return err + if p.stack.TesseraEnabled { + l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %d", i)) + tesseraEntrypointOutputDirectory := filepath.Join(initDir, "tessera", fmt.Sprintf("tessera_%d", i)) + if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, p.stack.Name, len(p.stack.Members)); err != nil { + return err + } } // Generate quorum docker-entrypoint for each member @@ -125,7 +128,7 @@ func (p *QuorumProvider) FirstTimeSetup() error { // Copy connector config to each member's volume connectorConfigPath := filepath.Join(p.stack.StackDir, "runtime", "config", fmt.Sprintf("%s_%v.yaml", p.connector.Name(), i)) connectorConfigVolumeName := fmt.Sprintf("%s_%s_config_%v", p.stack.Name, p.connector.Name(), i) - if err := docker.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, connectorConfigVolumeName, connectorConfigPath, "config.yaml"); err != nil { return err } @@ -135,39 +138,39 @@ func (p *QuorumProvider) FirstTimeSetup() error { // Copy the wallet files of each member to their respective blockchain volume keystoreDirectory := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, keystoreDirectory, "/"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, keystoreDirectory, "/"); err != nil { return err } if p.stack.TesseraEnabled { // Copy member specific tessera key files - if err := docker.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { + if err := p.dockerMgr.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { return err } tmKeystoreDirectory := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), "keystore") - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmKeystoreDirectory, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmKeystoreDirectory, rootDir); err != nil { return err } // Copy tessera docker-entrypoint file tmEntrypointPath := filepath.Join(tesseraDir, fmt.Sprintf("tessera_%d", i), DockerEntrypoint) - if err := docker.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmEntrypointPath, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, tesseraVolumeNameMember, tmEntrypointPath, rootDir); err != nil { return err } } // Copy quorum docker-entrypoint file quorumEntrypointPath := filepath.Join(blockchainDir, fmt.Sprintf("quorum_%d", i), DockerEntrypoint) - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, quorumEntrypointPath, rootDir); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, quorumEntrypointPath, rootDir); err != nil { return err } // Copy the genesis block information - if err := docker.CopyFileToVolume(p.ctx, quorumVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { + if err := p.dockerMgr.CopyFileToVolume(p.ctx, quorumVolumeNameMember, path.Join(blockchainDir, "genesis.json"), "genesis.json"); err != nil { return err } // Initialize the genesis block - if err := docker.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", quorumVolumeNameMember), quorumImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { + if err := p.dockerMgr.RunDockerCommand(p.ctx, p.stack.StackDir, "run", "--rm", "-v", fmt.Sprintf("%s:/data", quorumVolumeNameMember), quorumImage, "--datadir", "/data", "init", "/data/genesis.json"); err != nil { return err } } @@ -190,6 +193,7 @@ func (p *QuorumProvider) PostStart(firstTimeSetup bool) error { for _, member := range p.stack.Members { if member.Account.(*ethereum.Account).Address == address { memberIndex = *member.Index + break } } if err := p.unlockAccount(address, keyPassword, memberIndex); err != nil { @@ -204,7 +208,7 @@ func (p *QuorumProvider) unlockAccount(address, password string, memberIndex int l := log.LoggerFromContext(p.ctx) verbose := log.VerbosityFromContext(p.ctx) // exposed blockchain port is the default for node 0, we need to add the port multiplier to get the right rpc for the correct node - quorumClient := NewQuorumClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*exposedBlockchainPortMultiplier))) + quorumClient := NewQuorumClient(fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(memberIndex*ExposedBlockchainPortMultiplier))) retries := 10 for { if err := quorumClient.UnlockAccount(address, password); err != nil { @@ -250,7 +254,7 @@ func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefiniti ContainerName: fmt.Sprintf("%s_member%dtessera", p.stack.Name, i), Volumes: []string{fmt.Sprintf("tessera_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*exposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 + Ports: []string{fmt.Sprintf("%d:%s", p.stack.ExposedPtmPort+(i*ExposedBlockchainPortMultiplier), TmTpPort)}, // defaults 4100, 4110, 4120, 4130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, Deploy: map[string]interface{}{"restart_policy": map[string]string{"condition": "on-failure", "max_attempts": "3"}}, @@ -265,7 +269,7 @@ func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefiniti ContainerName: fmt.Sprintf("%s_quorum_%d", p.stack.Name, i), Volumes: []string{fmt.Sprintf("quorum_%d:/data", i)}, Logging: docker.StandardLogOptions, - Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*exposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 + Ports: []string{fmt.Sprintf("%d:8545", p.stack.ExposedBlockchainPort+(i*ExposedBlockchainPortMultiplier))}, // defaults 5100, 5110, 5120, 5130 Environment: p.stack.EnvironmentVars, EntryPoint: []string{"/bin/sh", "-c", "/data/docker-entrypoint.sh"}, DependsOn: quorumDependsOn, diff --git a/internal/blockchain/ethereum/quorum/quorum_provider_test.go b/internal/blockchain/ethereum/quorum/quorum_provider_test.go index 34282829..65fc7c01 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider_test.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider_test.go @@ -3,15 +3,19 @@ package quorum import ( "context" "encoding/hex" + "fmt" "os" "path/filepath" "testing" "github.com/hyperledger/firefly-cli/internal/blockchain/ethereum" + "github.com/hyperledger/firefly-cli/internal/docker/mocks" "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/internal/utils" "github.com/hyperledger/firefly-cli/pkg/types" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) @@ -42,12 +46,12 @@ func TestNewQuorumProvider(t *testing.T) { NodeName: "quorum", Account: ðereum.Account{ Address: "0x1234567890abcdef012345670000000000000000", - PrivateKey: "9876543210987654321098765432109876543210987654321098765432109876", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", }, }, }, - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Evmconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "evmconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, @@ -66,8 +70,8 @@ func TestNewQuorumProvider(t *testing.T) { }, }, }, - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "Ethconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockchainConnector", "ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), }, }, @@ -102,25 +106,25 @@ func TestParseAccount(t *testing.T) { { Name: "Account 2", Address: map[string]interface{}{ - "address": "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", - "privateKey": "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + "address": "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + "privateKey": "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", "ptmPublicKey": "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", }, ExpectedAccount: ðereum.Account{ - Address: "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", - PrivateKey: "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", }, }, { Name: "Account 3", Address: map[string]interface{}{ - "address": "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", - "privateKey": "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + "address": "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + "privateKey": "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", }, ExpectedAccount: ðereum.Account{ - Address: "0x549b5f43a40e1a0522864a004cfff2b0ca473a65", - PrivateKey: "112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", }, }, } @@ -285,40 +289,57 @@ func TestGetConnectorExternal(t *testing.T) { func TestCreateAccount(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) - testcases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - Args []string + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string }{ { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), + TesseraEnabled: false, }, - Args: []string{"Org-1_quorum", "Org-1_quorum", "0", "1"}, + Args: []string{"Org-1_quorum", "Org-1_quorum", "0"}, }, { - Name: "testcase1", + Name: "testcase2", Ctx: ctx, Stack: &types.Stack{ Name: "Org-2_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + }, + Args: []string{"Org-2_quorum", "Org-2_quorum", "1"}, + }, + { + Name: "testcase3", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-3_quorum", BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "Ethconnect"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "EvmConnect"), BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), InitDir: t.TempDir(), RuntimeDir: t.TempDir(), + TesseraEnabled: true, }, - Args: []string{"Org-2_quorum", "Org-2_quorum", "1", "2"}, + Args: []string{"Org-3_quorum", "Org-3_quorum", "1"}, }, } - for _, tc := range testcases { + for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { p := NewQuorumProvider(tc.Ctx, tc.Stack) Account, err := p.CreateAccount(tc.Args) @@ -345,3 +366,245 @@ func TestCreateAccount(t *testing.T) { }) } } + +func TestPostStart(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string + }{ + { + Name: "testcase1", + Ctx: ctx, + Stack: &types.Stack{ + State: &types.StackState{ + DeployedContracts: make([]*types.DeployedContract, 0), + }, + ExposedBlockchainPort: 8545, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Index: &[]int{1}[0], + Account: ðereum.Account{ + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + accounts := make([]interface{}, len(tc.Stack.Members)) + for memberIndex, member := range tc.Stack.Members { + accounts[memberIndex] = member.Account + + } + tc.Stack.State.Accounts = accounts + p := NewQuorumProvider(tc.Ctx, tc.Stack) + utils.StartMockServer(t) + // mock quorum rpc response during the unlocking of accounts + for _, member := range tc.Stack.Members { + rpcUrl := fmt.Sprintf("http://127.0.0.1:%v", p.stack.ExposedBlockchainPort+(*member.Index*ExposedBlockchainPortMultiplier)) + httpmock.RegisterResponder( + "POST", + rpcUrl, + httpmock.NewStringResponder(200, "{\"JSONRPC\": \"2.0\"}")) + } + httpmock.Activate() + hasRunBefore, _ := p.stack.HasRunBefore() + err := p.PostStart(hasRunBefore) + assert.Nil(t, err, "post start should not have an error") + utils.StopMockServer(t) + }) + } +} + +func TestFirstTimeSetup(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Args []string + }{ + { + Name: "testcase1_no_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + }, + }, + { + Name: "testcase2_with_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + }, + { + Index: &[]int{1}[0], + }, + }, + }, + }, + { + Name: "testcase3_with_members_and_tessera_enabled", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: true, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + p := NewQuorumProvider(tc.Ctx, tc.Stack) + p.dockerMgr = mocks.NewDockerManager() // docker related functionality should be tested in docker package + err := p.FirstTimeSetup() + assert.Nil(t, err, "first time setup should not throw an error") + }) + } +} + +func TestWriteConfig(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Ctx context.Context + Stack *types.Stack + TesseraEnabled bool + Options *types.InitOptions + }{ + { + Name: "testcase1_no_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + }, + Options: &types.InitOptions{ + BlockPeriod: 5, + }, + }, + { + Name: "testcase2_with_members", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: false, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Index: &[]int{1}[0], + Account: ðereum.Account{ + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + }, + }, + Options: &types.InitOptions{ + BlockPeriod: 5, + ExtraConnectorConfigPath: "", + }, + }, + { + Name: "testcase3_with_members_and_tessera_enabled", + Ctx: ctx, + Stack: &types.Stack{ + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + TesseraEnabled: true, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Index: &[]int{1}[0], + Account: ðereum.Account{ + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + }, + }, + Options: &types.InitOptions{ + BlockPeriod: 5, + ExtraConnectorConfigPath: "", + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + p := NewQuorumProvider(tc.Ctx, tc.Stack) + err := p.WriteConfig(tc.Options) + assert.Nil(t, err, "writing config should not throw an error") + }) + } +} diff --git a/internal/blockchain/ethereum/quorum/quorum_test.go b/internal/blockchain/ethereum/quorum/quorum_test.go new file mode 100644 index 00000000..e195a296 --- /dev/null +++ b/internal/blockchain/ethereum/quorum/quorum_test.go @@ -0,0 +1,72 @@ +package quorum + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hyperledger/firefly-cli/internal/log" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestCreateQuorumEntrypoint(t *testing.T) { + ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) + testCases := []struct { + Name string + Stack *types.Stack + Consensus string + StackName string + MemberIndex int + ChainID int + BlockPeriodInSeconds int + TesseraEnabled bool + }{ + { + Name: "testcase1", + Stack: &types.Stack{ + Name: "Org-1_quorum", + InitDir: t.TempDir(), + }, + Consensus: "ibft", + StackName: "org1", + MemberIndex: 0, + ChainID: 1337, + BlockPeriodInSeconds: -1, + TesseraEnabled: true, + }, + { + Name: "testcase2", + Stack: &types.Stack{ + Name: "Org-2_quorum", + InitDir: t.TempDir(), + }, + Consensus: "clique", + StackName: "org2", + MemberIndex: 1, + ChainID: 1337, + BlockPeriodInSeconds: 3, + TesseraEnabled: false, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + err := CreateQuorumEntrypoint(ctx, tc.Stack.InitDir, tc.Consensus, tc.StackName, tc.MemberIndex, tc.ChainID, tc.BlockPeriodInSeconds, tc.TesseraEnabled) + if err != nil { + t.Log("unable to create quorum docker entrypoint", err) + } + path := filepath.Join(tc.Stack.InitDir, "docker-entrypoint.sh") + _, err = os.Stat(path) + assert.NoError(t, err, "docker entrypoint file not created") + + b, err := os.ReadFile(path) + assert.NoError(t, err, "unable to read docker entrypoint file") + output := string(b) + strings.Contains(output, fmt.Sprintf("member%dtessera", tc.MemberIndex)) + strings.Contains(output, fmt.Sprintf("GOQUORUM_CONS_ALGO=%s", tc.Consensus)) + }) + } +} diff --git a/internal/docker/docker_manager.go b/internal/docker/docker_manager.go new file mode 100644 index 00000000..ae21ce26 --- /dev/null +++ b/internal/docker/docker_manager.go @@ -0,0 +1,104 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docker + +import ( + "context" +) + +// DockerInterface combines all Docker-related operations into a single interface. +type IDockerManager interface { + // Command Execution + RunDockerCommand(ctx context.Context, workingDir string, command ...string) error + RunDockerCommandLine(ctx context.Context, workingDir string, command string) error + RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error + RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) + RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) + + // Image Inspection + GetImageConfig(image string) (map[string]interface{}, error) + GetImageLabel(image, label string) (string, error) + GetImageDigest(image string) (string, error) + + // Volume Management + CreateVolume(ctx context.Context, volumeName string) error + CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error + MkdirInVolume(ctx context.Context, volumeName string, directory string) error + RemoveVolume(ctx context.Context, volumeName string) error + + // Container Interaction + CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error +} + +// DockerManager implements IDockerManager +type DockerManager struct{} + +func NewDockerManager() *DockerManager { + return &DockerManager{} +} + +func (mgr *DockerManager) RunDockerCommand(ctx context.Context, workingDir string, command ...string) error { + return RunDockerCommand(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerCommandLine(ctx context.Context, workingDir string, command string) error { + return RunDockerCommandLine(ctx, workingDir, command) +} + +func (mgr *DockerManager) RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error { + return RunDockerComposeCommand(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) { + return RunDockerCommandBuffered(ctx, workingDir, command...) +} + +func (mgr *DockerManager) RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) { + return RunDockerComposeCommandReturnsStdout(workingDir, command...) +} + +func (mgr *DockerManager) GetImageConfig(image string) (map[string]interface{}, error) { + return GetImageConfig(image) +} + +func (mgr *DockerManager) GetImageLabel(image, label string) (string, error) { + return GetImageLabel(image, label) +} + +func (mgr *DockerManager) GetImageDigest(image string) (string, error) { + return GetImageDigest(image) +} + +func (mgr *DockerManager) CreateVolume(ctx context.Context, volumeName string) error { + return CreateVolume(ctx, volumeName) +} + +func (mgr *DockerManager) CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error { + return CopyFileToVolume(ctx, volumeName, sourcePath, destPath) +} + +func (mgr *DockerManager) MkdirInVolume(ctx context.Context, volumeName string, directory string) error { + return MkdirInVolume(ctx, volumeName, directory) +} + +func (mgr *DockerManager) RemoveVolume(ctx context.Context, volumeName string) error { + return RemoveVolume(ctx, volumeName) +} + +func (mgr *DockerManager) CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error { + return CopyFromContainer(ctx, containerName, sourcePath, destPath) +} diff --git a/internal/docker/mocks/docker_manager.go b/internal/docker/mocks/docker_manager.go new file mode 100644 index 00000000..982a8d94 --- /dev/null +++ b/internal/docker/mocks/docker_manager.go @@ -0,0 +1,62 @@ +// DockerManager is a mock that implements IDockerManager +package mocks + +import "context" + +type DockerManager struct{} + +func NewDockerManager() *DockerManager { + return &DockerManager{} +} + +func (mgr *DockerManager) RunDockerCommand(ctx context.Context, workingDir string, command ...string) error { + return nil +} + +func (mgr *DockerManager) RunDockerCommandLine(ctx context.Context, workingDir string, command string) error { + return nil +} + +func (mgr *DockerManager) RunDockerComposeCommand(ctx context.Context, workingDir string, command ...string) error { + return nil +} + +func (mgr *DockerManager) RunDockerCommandBuffered(ctx context.Context, workingDir string, command ...string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) RunDockerComposeCommandReturnsStdout(workingDir string, command ...string) ([]byte, error) { + return nil, nil +} + +func (mgr *DockerManager) GetImageConfig(image string) (map[string]interface{}, error) { + return nil, nil +} + +func (mgr *DockerManager) GetImageLabel(image, label string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) GetImageDigest(image string) (string, error) { + return "", nil +} + +func (mgr *DockerManager) CreateVolume(ctx context.Context, volumeName string) error { + return nil +} + +func (mgr *DockerManager) CopyFileToVolume(ctx context.Context, volumeName string, sourcePath string, destPath string) error { + return nil +} + +func (mgr *DockerManager) MkdirInVolume(ctx context.Context, volumeName string, directory string) error { + return nil +} + +func (mgr *DockerManager) RemoveVolume(ctx context.Context, volumeName string) error { + return nil +} + +func (mgr *DockerManager) CopyFromContainer(ctx context.Context, containerName string, sourcePath string, destPath string) error { + return nil +} From 608c8388c7272525d82f7977a3f1fd628fee2468 Mon Sep 17 00:00:00 2001 From: rodion Date: Tue, 2 Jul 2024 10:27:46 +0000 Subject: [PATCH 20/22] Update gas limit for quorum Signed-off-by: rodion --- internal/blockchain/ethereum/quorum/genesis.go | 2 +- internal/blockchain/ethereum/quorum/genesis_test.go | 2 +- internal/blockchain/ethereum/quorum/quorum.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/blockchain/ethereum/quorum/genesis.go b/internal/blockchain/ethereum/quorum/genesis.go index e9751dd1..e111a735 100644 --- a/internal/blockchain/ethereum/quorum/genesis.go +++ b/internal/blockchain/ethereum/quorum/genesis.go @@ -95,7 +95,7 @@ func CreateGenesis(addresses []string, blockPeriod int, chainID int64) *Genesis Nonce: "0x0", Timestamp: "0x0", ExtraData: extraData, - GasLimit: "0xffffff", + GasLimit: "0xE0000000", Difficulty: "0x1", MixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", Coinbase: "0x0000000000000000000000000000000000000000", diff --git a/internal/blockchain/ethereum/quorum/genesis_test.go b/internal/blockchain/ethereum/quorum/genesis_test.go index 2a396113..59753b8e 100644 --- a/internal/blockchain/ethereum/quorum/genesis_test.go +++ b/internal/blockchain/ethereum/quorum/genesis_test.go @@ -77,7 +77,7 @@ func TestCreateGenesis(t *testing.T) { Nonce: "0x0", Timestamp: "0x0", ExtraData: extraData, - GasLimit: "0xffffff", + GasLimit: "0xE0000000", Difficulty: "0x1", MixHash: "0x0000000000000000000000000000000000000000000000000000000000000000", Coinbase: "0x0000000000000000000000000000000000000000", diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index 9bb2bcb6..8029b3cc 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -92,7 +92,7 @@ ADDITIONAL_ARGS=${ADDITIONAL_ARGS:-} echo "bootnode discovery command :: $BOOTNODE_CMD" IP_ADDR=$(cat /etc/hosts | tail -n 1 | awk '{print $1}') -exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %[4]s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %[5]d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS --miner.gaslimit 16777215 $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, QuorumPort, chainID, blockPeriod, blockPeriodInMs) +exec geth --datadir /data --nat extip:$IP_ADDR --syncmode 'full' --revertreason --port 30311 --http --http.addr "0.0.0.0" --http.corsdomain="*" -http.port %[4]s --http.vhosts "*" --http.api admin,personal,eth,net,web3,txpool,miner,debug,$QUORUM_API --networkid %[5]d --miner.gasprice 0 --password /data/password --mine --allow-insecure-unlock --verbosity 4 $CONSENSUS_ARGS $BOOTNODE_CMD $ADDITIONAL_ARGS`, consensus, tesseraCmd, discoveryCmd, QuorumPort, chainID, blockPeriod, blockPeriodInMs) filename := filepath.Join(outputDirectory, DockerEntrypoint) if err := os.MkdirAll(outputDirectory, 0755); err != nil { return err From 18dc785a02ec23acfe84d168b02ddff76b3847ab Mon Sep 17 00:00:00 2001 From: rodion Date: Wed, 10 Jul 2024 02:09:33 +0000 Subject: [PATCH 21/22] Make consensus and tessera args generic Signed-off-by: rodion --- cmd/init.go | 27 ++- internal/blockchain/ethereum/quorum/quorum.go | 6 +- .../ethereum/quorum/quorum_provider.go | 12 +- .../ethereum/quorum/quorum_provider_test.go | 202 ++++++++++-------- .../blockchain/ethereum/quorum/quorum_test.go | 45 ++-- internal/stacks/stack_manager.go | 28 +-- pkg/types/options.go | 93 ++++---- pkg/types/stack.go | 66 +++--- 8 files changed, 262 insertions(+), 217 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index e6084fcb..596a9c7e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -82,10 +82,10 @@ func initCommon(args []string) error { if err := validateIPFSMode(initOptions.IPFSMode); err != nil { return err } - if err := validateQuorumConsensus(initOptions.QuorumConsensus); err != nil { + if err := validateConsensus(initOptions.Consensus); err != nil { return err } - if err := validateTesseraSelection(initOptions.TesseraEnabled, initOptions.BlockchainNodeProvider); err != nil { + if err := validatePrivateTransactionManagerSelection(initOptions.PrivateTransactionManager, initOptions.BlockchainNodeProvider); err != nil { return err } @@ -206,28 +206,33 @@ func validateBlockchainProvider(providerString, nodeString string) error { return nil } -func validateQuorumConsensus(consensusString string) error { - v, err := fftypes.FFEnumParseString(context.Background(), types.QuorumConsensus, consensusString) +func validateConsensus(consensusString string) error { + v, err := fftypes.FFEnumParseString(context.Background(), types.Consensus, consensusString) if err != nil { return nil } - if v != types.QuorumConsensusClique { - return errors.New("consensus algorithms such as raft/ibft/qbft for quorum not supported") + if v != types.ConsensusClique { + return errors.New("currently only Clique consensus is supported") } return nil } -func validateTesseraSelection(tesseraEnabled bool, nodeString string) error { - if tesseraEnabled { +func validatePrivateTransactionManagerSelection(privateTransactionManagerInput string, nodeString string) error { + privateTransactionManager, err := fftypes.FFEnumParseString(context.Background(), types.PrivateTransactionManager, privateTransactionManagerInput) + if err != nil { + return err + } + + if !privateTransactionManager.Equals(types.PrivateTransactionManagerNone) { v, err := fftypes.FFEnumParseString(context.Background(), types.BlockchainNodeProvider, nodeString) if err != nil { return nil } if v != types.BlockchainNodeProviderQuorum { - return errors.New("tessera can only be enabled if blockchain node provider is quorum") + return errors.New("private transaction manager can only be enabled if blockchain node provider is quorum") } } return nil @@ -284,8 +289,8 @@ func init() { initCmd.Flags().StringVarP(&initOptions.BlockchainConnector, "blockchain-connector", "c", "evmconnect", fmt.Sprintf("Blockchain connector to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainConnector))) initCmd.Flags().StringVarP(&initOptions.BlockchainProvider, "blockchain-provider", "b", "ethereum", fmt.Sprintf("Blockchain to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainProvider))) initCmd.Flags().StringVarP(&initOptions.BlockchainNodeProvider, "blockchain-node", "n", "geth", fmt.Sprintf("Blockchain node type to use. Options are: %v", fftypes.FFEnumValues(types.BlockchainNodeProvider))) - initCmd.PersistentFlags().BoolVar(&initOptions.TesseraEnabled, "tessera-enabled", false, "Enables private transaction manager Tessera to start alongside with Quorum") - initCmd.PersistentFlags().StringVar(&initOptions.QuorumConsensus, "quorum-consensus", "clique", fmt.Sprintf("Consensus algorithm used when Blockchain node type is Quorum. Options are %v", fftypes.FFEnumValues(types.QuorumConsensus))) + initCmd.PersistentFlags().StringVar(&initOptions.PrivateTransactionManager, "private-transaction-manager", "none", fmt.Sprintf("Private Transaction Manager to use. Options are: %v", fftypes.FFEnumValues(types.PrivateTransactionManager))) + initCmd.PersistentFlags().StringVar(&initOptions.Consensus, "consensus", "clique", fmt.Sprintf("Consensus algorithm to use. Options are %v", fftypes.FFEnumValues(types.Consensus))) initCmd.PersistentFlags().StringArrayVarP(&initOptions.TokenProviders, "token-providers", "t", []string{"erc20_erc721"}, fmt.Sprintf("Token providers to use. Options are: %v", fftypes.FFEnumValues(types.TokenProvider))) initCmd.PersistentFlags().IntVarP(&initOptions.ExternalProcesses, "external", "e", 0, "Manage a number of FireFly core processes outside of the docker-compose stack - useful for development and debugging") initCmd.PersistentFlags().StringVarP(&initOptions.FireFlyVersion, "release", "r", "latest", fmt.Sprintf("Select the FireFly release version to use. Options are: %v", fftypes.FFEnumValues(types.ReleaseChannelSelection))) diff --git a/internal/blockchain/ethereum/quorum/quorum.go b/internal/blockchain/ethereum/quorum/quorum.go index 8029b3cc..06bd2fd8 100644 --- a/internal/blockchain/ethereum/quorum/quorum.go +++ b/internal/blockchain/ethereum/quorum/quorum.go @@ -23,9 +23,11 @@ import ( "path/filepath" "github.com/hyperledger/firefly-cli/internal/docker" + "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/hyperledger/firefly-common/pkg/fftypes" ) -func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, consensus, stackName string, memberIndex, chainID, blockPeriodInSeconds int, tesseraEnabled bool) error { +func CreateQuorumEntrypoint(ctx context.Context, outputDirectory, consensus, stackName string, memberIndex, chainID, blockPeriodInSeconds int, privateTransactionManager fftypes.FFEnum) error { discoveryCmd := "BOOTNODE_CMD=\"\"" connectTimeout := 15 if memberIndex != 0 { @@ -35,7 +37,7 @@ BOOTNODE_CMD=${BOOTNODE_CMD/127.0.0.1/quorum_0}`, QuorumPort, connectTimeout) } tesseraCmd := "" - if tesseraEnabled { + if !privateTransactionManager.Equals(types.PrivateTransactionManagerNone) { tesseraCmd = fmt.Sprintf(`TESSERA_URL=http://%[5]s_member%[1]dtessera TESSERA_TP_PORT=%[2]s TESSERA_Q2T_PORT=%[3]s diff --git a/internal/blockchain/ethereum/quorum/quorum_provider.go b/internal/blockchain/ethereum/quorum/quorum_provider.go index efe541cb..0e3052db 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider.go @@ -77,7 +77,7 @@ func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { } // Generate tessera docker-entrypoint for each member - if p.stack.TesseraEnabled { + if !p.stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerNone) { l.Info(fmt.Sprintf("generating tessera docker-entrypoint file for member %d", i)) tesseraEntrypointOutputDirectory := filepath.Join(initDir, "tessera", fmt.Sprintf("tessera_%d", i)) if err := CreateTesseraEntrypoint(p.ctx, tesseraEntrypointOutputDirectory, p.stack.Name, len(p.stack.Members)); err != nil { @@ -88,7 +88,7 @@ func (p *QuorumProvider) WriteConfig(options *types.InitOptions) error { // Generate quorum docker-entrypoint for each member l.Info(fmt.Sprintf("generating quorum docker-entrypoint file for member %d", i)) quorumEntrypointOutputDirectory := filepath.Join(initDir, "blockchain", fmt.Sprintf("quorum_%d", i)) - if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, p.stack.QuorumConsensus.String(), p.stack.Name, i, int(p.stack.ChainID()), options.BlockPeriod, p.stack.TesseraEnabled); err != nil { + if err := CreateQuorumEntrypoint(p.ctx, quorumEntrypointOutputDirectory, p.stack.Consensus.String(), p.stack.Name, i, int(p.stack.ChainID()), options.BlockPeriod, p.stack.PrivateTransactionManager); err != nil { return err } } @@ -142,7 +142,7 @@ func (p *QuorumProvider) FirstTimeSetup() error { return err } - if p.stack.TesseraEnabled { + if !p.stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerNone) { // Copy member specific tessera key files if err := p.dockerMgr.MkdirInVolume(p.ctx, tesseraVolumeNameMember, rootDir); err != nil { return err @@ -238,14 +238,14 @@ func (p *QuorumProvider) DeployFireFlyContract() (*types.ContractDeploymentResul func (p *QuorumProvider) GetDockerServiceDefinitions() []*docker.ServiceDefinition { memberCount := len(p.stack.Members) serviceDefinitionsCount := memberCount - if p.stack.TesseraEnabled { + if !p.stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerNone) { serviceDefinitionsCount *= 2 } serviceDefinitions := make([]*docker.ServiceDefinition, serviceDefinitionsCount) connectorDependents := map[string]string{} for i := 0; i < memberCount; i++ { var quorumDependsOn map[string]map[string]string - if p.stack.TesseraEnabled { + if !p.stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerNone) { quorumDependsOn = map[string]map[string]string{fmt.Sprintf("tessera_%d", i): {"condition": "service_started"}} serviceDefinitions[i+memberCount] = &docker.ServiceDefinition{ ServiceName: fmt.Sprintf("tessera_%d", i), @@ -361,7 +361,7 @@ func (p *QuorumProvider) CreateAccount(args []string) (interface{}, error) { // Tessera is an optional add-on to the quorum blockchain node provider var tesseraPubKey, tesseraKeysPath string - if p.stack.TesseraEnabled { + if !p.stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerNone) { tesseraKeysOutputDirectory := filepath.Join(directory, "tessera", fmt.Sprintf("tessera_%s", memberIndex), "keystore") _, tesseraPubKey, tesseraKeysPath, err = CreateTesseraKeys(p.ctx, tesseraImage, tesseraKeysOutputDirectory, "", "tm") if err != nil { diff --git a/internal/blockchain/ethereum/quorum/quorum_provider_test.go b/internal/blockchain/ethereum/quorum/quorum_provider_test.go index 65fc7c01..2bee16cc 100644 --- a/internal/blockchain/ethereum/quorum/quorum_provider_test.go +++ b/internal/blockchain/ethereum/quorum/quorum_provider_test.go @@ -290,23 +290,23 @@ func TestGetConnectorExternal(t *testing.T) { func TestCreateAccount(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testCases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - TesseraEnabled bool - Args []string + Name string + Ctx context.Context + Stack *types.Stack + PrivateTransactionManager fftypes.FFEnum + Args []string }{ { Name: "testcase1", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, }, Args: []string{"Org-1_quorum", "Org-1_quorum", "0"}, }, @@ -314,13 +314,13 @@ func TestCreateAccount(t *testing.T) { Name: "testcase2", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-2_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-2_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "ethconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, }, Args: []string{"Org-2_quorum", "Org-2_quorum", "1"}, }, @@ -328,13 +328,13 @@ func TestCreateAccount(t *testing.T) { Name: "testcase3", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-3_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "EvmConnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: true, + Name: "Org-3_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "Ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "EvmConnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerTessera, }, Args: []string{"Org-3_quorum", "Org-3_quorum", "1"}, }, @@ -358,7 +358,7 @@ func TestCreateAccount(t *testing.T) { _, err = hex.DecodeString(account.PrivateKey) assert.NoError(t, err, "invalid private key format") // Check if the tessera public key is a non-empty string - if tc.Stack.TesseraEnabled { + if tc.Stack.PrivateTransactionManager.Equals(types.PrivateTransactionManagerTessera) { assert.NotEmpty(t, account.PtmPublicKey) } else { assert.Empty(t, account.PtmPublicKey) @@ -370,20 +370,50 @@ func TestCreateAccount(t *testing.T) { func TestPostStart(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testCases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - TesseraEnabled bool - Args []string + Name string + Ctx context.Context + Stack *types.Stack + PrivateTransactionManager fftypes.FFEnum + Args []string }{ { - Name: "testcase1", + Name: "testcase1_with_tessera_enabled", + Ctx: ctx, + Stack: &types.Stack{ + State: &types.StackState{ + DeployedContracts: make([]*types.DeployedContract, 0), + }, + ExposedBlockchainPort: 8545, + PrivateTransactionManager: types.PrivateTransactionManagerTessera, + Members: []*types.Organization{ + { + Index: &[]int{0}[0], + Account: ðereum.Account{ + Address: "0x1234567890abcdef0123456789abcdef6789abcd", + PrivateKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + { + Index: &[]int{1}[0], + Account: ðereum.Account{ + Address: "0x618E98197aF52F44D1B05Af0952a59b9f702dea4", + PrivateKey: "1b2b1ac0127957bb57e914993c47bfd69c5b0acc86425ee8ab2108f684a68a15", + PtmPublicKey: "SBEV8qc12zSe7XfhqSChloYryb5aDK0XdBF3IwxZADE=", + }, + }, + }, + }, + }, + { + Name: "testcase2_with_tessera_disabled", Ctx: ctx, Stack: &types.Stack{ State: &types.StackState{ DeployedContracts: make([]*types.DeployedContract, 0), }, - ExposedBlockchainPort: 8545, + ExposedBlockchainPort: 8545, + PrivateTransactionManager: types.PrivateTransactionManagerNone, Members: []*types.Organization{ { Index: &[]int{0}[0], @@ -435,36 +465,36 @@ func TestPostStart(t *testing.T) { func TestFirstTimeSetup(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testCases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - TesseraEnabled bool - Args []string + Name string + Ctx context.Context + Stack *types.Stack + PrivateTransactionManager fftypes.FFEnum + Args []string }{ { Name: "testcase1_no_members", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, }, }, { Name: "testcase2_with_members", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, Members: []*types.Organization{ { Index: &[]int{0}[0], @@ -479,13 +509,13 @@ func TestFirstTimeSetup(t *testing.T) { Name: "testcase3_with_members_and_tessera_enabled", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: true, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerTessera, Members: []*types.Organization{ { Index: &[]int{0}[0], @@ -507,23 +537,23 @@ func TestFirstTimeSetup(t *testing.T) { func TestWriteConfig(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testCases := []struct { - Name string - Ctx context.Context - Stack *types.Stack - TesseraEnabled bool - Options *types.InitOptions + Name string + Ctx context.Context + Stack *types.Stack + PrivateTransactionManager fftypes.FFEnum + Options *types.InitOptions }{ { Name: "testcase1_no_members", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, }, Options: &types.InitOptions{ BlockPeriod: 5, @@ -533,13 +563,13 @@ func TestWriteConfig(t *testing.T) { Name: "testcase2_with_members", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: false, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerNone, Members: []*types.Organization{ { Index: &[]int{0}[0], @@ -568,13 +598,13 @@ func TestWriteConfig(t *testing.T) { Name: "testcase3_with_members_and_tessera_enabled", Ctx: ctx, Stack: &types.Stack{ - Name: "Org-1_quorum", - BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), - BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), - BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), - InitDir: t.TempDir(), - RuntimeDir: t.TempDir(), - TesseraEnabled: true, + Name: "Org-1_quorum", + BlockchainProvider: fftypes.FFEnumValue("BlockchainProvider", "ethereum"), + BlockchainConnector: fftypes.FFEnumValue("BlockChainConnector", "evmconnect"), + BlockchainNodeProvider: fftypes.FFEnumValue("BlockchainNodeProvider", "quorum"), + InitDir: t.TempDir(), + RuntimeDir: t.TempDir(), + PrivateTransactionManager: types.PrivateTransactionManagerTessera, Members: []*types.Organization{ { Index: &[]int{0}[0], diff --git a/internal/blockchain/ethereum/quorum/quorum_test.go b/internal/blockchain/ethereum/quorum/quorum_test.go index e195a296..df1f0208 100644 --- a/internal/blockchain/ethereum/quorum/quorum_test.go +++ b/internal/blockchain/ethereum/quorum/quorum_test.go @@ -10,20 +10,21 @@ import ( "github.com/hyperledger/firefly-cli/internal/log" "github.com/hyperledger/firefly-cli/pkg/types" + "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/stretchr/testify/assert" ) func TestCreateQuorumEntrypoint(t *testing.T) { ctx := log.WithVerbosity(log.WithLogger(context.Background(), &log.StdoutLogger{}), false) testCases := []struct { - Name string - Stack *types.Stack - Consensus string - StackName string - MemberIndex int - ChainID int - BlockPeriodInSeconds int - TesseraEnabled bool + Name string + Stack *types.Stack + Consensus string + StackName string + MemberIndex int + ChainID int + BlockPeriodInSeconds int + PrivateTransactionManager fftypes.FFEnum }{ { Name: "testcase1", @@ -31,30 +32,30 @@ func TestCreateQuorumEntrypoint(t *testing.T) { Name: "Org-1_quorum", InitDir: t.TempDir(), }, - Consensus: "ibft", - StackName: "org1", - MemberIndex: 0, - ChainID: 1337, - BlockPeriodInSeconds: -1, - TesseraEnabled: true, + Consensus: "ibft", + StackName: "org1", + MemberIndex: 0, + ChainID: 1337, + BlockPeriodInSeconds: -1, + PrivateTransactionManager: types.PrivateTransactionManagerTessera, }, { - Name: "testcase2", + Name: "testcase2_with_tessera_disabled", Stack: &types.Stack{ Name: "Org-2_quorum", InitDir: t.TempDir(), }, - Consensus: "clique", - StackName: "org2", - MemberIndex: 1, - ChainID: 1337, - BlockPeriodInSeconds: 3, - TesseraEnabled: false, + Consensus: "clique", + StackName: "org2", + MemberIndex: 1, + ChainID: 1337, + BlockPeriodInSeconds: 3, + PrivateTransactionManager: types.PrivateTransactionManagerNone, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - err := CreateQuorumEntrypoint(ctx, tc.Stack.InitDir, tc.Consensus, tc.StackName, tc.MemberIndex, tc.ChainID, tc.BlockPeriodInSeconds, tc.TesseraEnabled) + err := CreateQuorumEntrypoint(ctx, tc.Stack.InitDir, tc.Consensus, tc.StackName, tc.MemberIndex, tc.ChainID, tc.BlockPeriodInSeconds, tc.PrivateTransactionManager) if err != nil { t.Log("unable to create quorum docker entrypoint", err) } diff --git a/internal/stacks/stack_manager.go b/internal/stacks/stack_manager.go index 54faff79..2fa45da6 100644 --- a/internal/stacks/stack_manager.go +++ b/internal/stacks/stack_manager.go @@ -95,20 +95,20 @@ func (s *StackManager) InitStack(options *types.InitOptions) (err error) { environmentVarsMap[key] = value } s.Stack = &types.Stack{ - Name: options.StackName, - Members: make([]*types.Organization, options.MemberCount), - ExposedBlockchainPort: options.ServicesBasePort, - ExposedPtmPort: options.PtmBasePort, - Database: fftypes.FFEnum(options.DatabaseProvider), - BlockchainProvider: fftypes.FFEnum(options.BlockchainProvider), - BlockchainNodeProvider: fftypes.FFEnum(options.BlockchainNodeProvider), - BlockchainConnector: fftypes.FFEnum(options.BlockchainConnector), - TesseraEnabled: options.TesseraEnabled, - QuorumConsensus: fftypes.FFEnum(options.QuorumConsensus), - ContractAddress: options.ContractAddress, - StackDir: filepath.Join(constants.StacksDir, options.StackName), - InitDir: filepath.Join(constants.StacksDir, options.StackName, "init"), - RuntimeDir: filepath.Join(constants.StacksDir, options.StackName, "runtime"), + Name: options.StackName, + Members: make([]*types.Organization, options.MemberCount), + ExposedBlockchainPort: options.ServicesBasePort, + ExposedPtmPort: options.PtmBasePort, + Database: fftypes.FFEnum(options.DatabaseProvider), + BlockchainProvider: fftypes.FFEnum(options.BlockchainProvider), + BlockchainNodeProvider: fftypes.FFEnum(options.BlockchainNodeProvider), + BlockchainConnector: fftypes.FFEnum(options.BlockchainConnector), + PrivateTransactionManager: fftypes.FFEnum(options.PrivateTransactionManager), + Consensus: fftypes.FFEnum(options.Consensus), + ContractAddress: options.ContractAddress, + StackDir: filepath.Join(constants.StacksDir, options.StackName), + InitDir: filepath.Join(constants.StacksDir, options.StackName, "init"), + RuntimeDir: filepath.Join(constants.StacksDir, options.StackName, "runtime"), State: &types.StackState{ DeployedContracts: make([]*types.DeployedContract, 0), Accounts: make([]interface{}, options.MemberCount), diff --git a/pkg/types/options.go b/pkg/types/options.go index 9633ceea..a43b0770 100644 --- a/pkg/types/options.go +++ b/pkg/types/options.go @@ -31,44 +31,44 @@ type StartOptions struct { } type InitOptions struct { - StackName string - MemberCount int - FireFlyBasePort int - ServicesBasePort int - PtmBasePort int - DatabaseProvider string - ExternalProcesses int - OrgNames []string - NodeNames []string - BlockchainConnector string - BlockchainProvider string - BlockchainNodeProvider string - TesseraEnabled bool - QuorumConsensus string - TokenProviders []string - FireFlyVersion string - ManifestPath string - PrometheusEnabled bool - PrometheusPort int - SandboxEnabled bool - ExtraCoreConfigPath string - ExtraConnectorConfigPath string - BlockPeriod int - ContractAddress string - RemoteNodeURL string - ChainID int64 - DisableTokenFactories bool - RequestTimeout int - ReleaseChannel string - MultipartyEnabled bool - IPFSMode string - CCPYAMLPaths []string - MSPPaths []string - ChannelName string - ChaincodeName string - CustomPinSupport bool - RemoteNodeDeploy bool - EnvironmentVars map[string]string + StackName string + MemberCount int + FireFlyBasePort int + ServicesBasePort int + PtmBasePort int + DatabaseProvider string + ExternalProcesses int + OrgNames []string + NodeNames []string + BlockchainConnector string + BlockchainProvider string + BlockchainNodeProvider string + PrivateTransactionManager string + Consensus string + TokenProviders []string + FireFlyVersion string + ManifestPath string + PrometheusEnabled bool + PrometheusPort int + SandboxEnabled bool + ExtraCoreConfigPath string + ExtraConnectorConfigPath string + BlockPeriod int + ContractAddress string + RemoteNodeURL string + ChainID int64 + DisableTokenFactories bool + RequestTimeout int + ReleaseChannel string + MultipartyEnabled bool + IPFSMode string + CCPYAMLPaths []string + MSPPaths []string + ChannelName string + ChaincodeName string + CustomPinSupport bool + RemoteNodeDeploy bool + EnvironmentVars map[string]string } const IPFSMode = "ipfs_mode" @@ -105,13 +105,20 @@ var ( BlockchainNodeProviderRemoteRPC = fftypes.FFEnumValue(BlockchainNodeProvider, "remote-rpc") ) -const QuorumConsensus = "quorum_consensus" +const Consensus = "consensus" var ( - QuorumConsensusClique = fftypes.FFEnumValue(QuorumConsensus, "clique") - QuorumConsensusRaft = fftypes.FFEnumValue(QuorumConsensus, "raft") - QuorumConsensusIbft = fftypes.FFEnumValue(QuorumConsensus, "ibft") - QuorumConsensusQbft = fftypes.FFEnumValue(QuorumConsensus, "qbft") + ConsensusClique = fftypes.FFEnumValue(Consensus, "clique") + ConsensusRaft = fftypes.FFEnumValue(Consensus, "raft") + ConsensusIbft = fftypes.FFEnumValue(Consensus, "ibft") + ConsensusQbft = fftypes.FFEnumValue(Consensus, "qbft") +) + +const PrivateTransactionManager = "private_transaction_manager" + +var ( + PrivateTransactionManagerNone = fftypes.FFEnumValue(PrivateTransactionManager, "none") + PrivateTransactionManagerTessera = fftypes.FFEnumValue(PrivateTransactionManager, "tessera") ) const DatabaseSelection = "database_selection" diff --git a/pkg/types/stack.go b/pkg/types/stack.go index 3581baff..c490c873 100644 --- a/pkg/types/stack.go +++ b/pkg/types/stack.go @@ -25,39 +25,39 @@ import ( ) type Stack struct { - Name string `json:"name,omitempty"` - Members []*Organization `json:"members,omitempty"` - SwarmKey string `json:"swarmKey,omitempty"` - ExposedBlockchainPort int `json:"exposedBlockchainPort,omitempty"` - ExposedPtmPort int `json:"exposedPtmPort,omitempty"` - Database fftypes.FFEnum `json:"database"` - BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` - BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` - BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` - TesseraEnabled bool `json:"tesseraEnabled"` - QuorumConsensus fftypes.FFEnum `json:"quorumConsensus"` - TokenProviders []fftypes.FFEnum `json:"tokenProviders"` - VersionManifest *VersionManifest `json:"versionManifest,omitempty"` - PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` - SandboxEnabled bool `json:"sandboxEnabled,omitempty"` - MultipartyEnabled bool `json:"multiparty"` - ExposedPrometheusPort int `json:"exposedPrometheusPort,omitempty"` - ContractAddress string `json:"contractAddress,omitempty"` - ChainIDPtr *int64 `json:"chainID,omitempty"` - RemoteNodeURL string `json:"remoteNodeURL,omitempty"` - DisableTokenFactories bool `json:"disableTokenFactories,omitempty"` - RequestTimeout int `json:"requestTimeout,omitempty"` - IPFSMode fftypes.FFEnum `json:"ipfsMode"` - RemoteFabricNetwork bool `json:"remoteFabricNetwork,omitempty"` - ChannelName string `json:"channelName,omitempty"` - ChaincodeName string `json:"chaincodeName,omitempty"` - CustomPinSupport bool `json:"customPinSupport,omitempty"` - RemoteNodeDeploy bool `json:"remoteNodeDeploy,omitempty"` - EnvironmentVars map[string]interface{} `json:"environmentVars"` - InitDir string `json:"-"` - RuntimeDir string `json:"-"` - StackDir string `json:"-"` - State *StackState `json:"-"` + Name string `json:"name,omitempty"` + Members []*Organization `json:"members,omitempty"` + SwarmKey string `json:"swarmKey,omitempty"` + ExposedBlockchainPort int `json:"exposedBlockchainPort,omitempty"` + ExposedPtmPort int `json:"exposedPtmPort,omitempty"` + Database fftypes.FFEnum `json:"database"` + BlockchainProvider fftypes.FFEnum `json:"blockchainProvider"` + BlockchainConnector fftypes.FFEnum `json:"blockchainConnector"` + BlockchainNodeProvider fftypes.FFEnum `json:"blockchainNodeProvider"` + PrivateTransactionManager fftypes.FFEnum `json:"privateTransactionManager"` + Consensus fftypes.FFEnum `json:"consensus"` + TokenProviders []fftypes.FFEnum `json:"tokenProviders"` + VersionManifest *VersionManifest `json:"versionManifest,omitempty"` + PrometheusEnabled bool `json:"prometheusEnabled,omitempty"` + SandboxEnabled bool `json:"sandboxEnabled,omitempty"` + MultipartyEnabled bool `json:"multiparty"` + ExposedPrometheusPort int `json:"exposedPrometheusPort,omitempty"` + ContractAddress string `json:"contractAddress,omitempty"` + ChainIDPtr *int64 `json:"chainID,omitempty"` + RemoteNodeURL string `json:"remoteNodeURL,omitempty"` + DisableTokenFactories bool `json:"disableTokenFactories,omitempty"` + RequestTimeout int `json:"requestTimeout,omitempty"` + IPFSMode fftypes.FFEnum `json:"ipfsMode"` + RemoteFabricNetwork bool `json:"remoteFabricNetwork,omitempty"` + ChannelName string `json:"channelName,omitempty"` + ChaincodeName string `json:"chaincodeName,omitempty"` + CustomPinSupport bool `json:"customPinSupport,omitempty"` + RemoteNodeDeploy bool `json:"remoteNodeDeploy,omitempty"` + EnvironmentVars map[string]interface{} `json:"environmentVars"` + InitDir string `json:"-"` + RuntimeDir string `json:"-"` + StackDir string `json:"-"` + State *StackState `json:"-"` } func (s *Stack) ChainID() int64 { From ae22e11bece517aad758cf29cc0447f1e73ac8d6 Mon Sep 17 00:00:00 2001 From: rodion Date: Wed, 10 Jul 2024 02:23:10 +0000 Subject: [PATCH 22/22] Disable evmconnect with tessera temporarily Signed-off-by: rodion --- cmd/init.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cmd/init.go b/cmd/init.go index 596a9c7e..6d00a362 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -88,6 +88,9 @@ func initCommon(args []string) error { if err := validatePrivateTransactionManagerSelection(initOptions.PrivateTransactionManager, initOptions.BlockchainNodeProvider); err != nil { return err } + if err := validatePrivateTransactionManagerBlockchainConnectorCombination(initOptions.PrivateTransactionManager, initOptions.BlockchainConnector); err != nil { + return err + } fmt.Println("initializing new FireFly stack...") @@ -232,7 +235,26 @@ func validatePrivateTransactionManagerSelection(privateTransactionManagerInput s } if v != types.BlockchainNodeProviderQuorum { - return errors.New("private transaction manager can only be enabled if blockchain node provider is quorum") + return errors.New("private transaction manager can only be enabled if blockchain node provider is Quorum") + } + } + return nil +} + +func validatePrivateTransactionManagerBlockchainConnectorCombination(privateTransactionManagerInput string, blockchainConnectorInput string) error { + privateTransactionManager, err := fftypes.FFEnumParseString(context.Background(), types.PrivateTransactionManager, privateTransactionManagerInput) + if err != nil { + return err + } + + blockchainConnector, err := fftypes.FFEnumParseString(context.Background(), types.BlockchainConnector, blockchainConnectorInput) + if err != nil { + return nil + } + + if !privateTransactionManager.Equals(types.PrivateTransactionManagerNone) { + if !blockchainConnector.Equals(types.BlockchainConnectorEthconnect) { + return errors.New("currently only Ethconnect blockchain connector is supported with a private transaction manager") } } return nil