diff --git a/.github/workflows/manual-recover-network-funds.yml b/.github/workflows/manual-recover-network-funds.yml new file mode 100644 index 0000000000..cb0f786a71 --- /dev/null +++ b/.github/workflows/manual-recover-network-funds.yml @@ -0,0 +1,69 @@ +# Run script to retrieve funds from an existing testnet deployment + +name: '[M] Recover Testnet Funds' +run-name: '[M] Recover Testnet Funds ( ${{ github.event.inputs.testnet_type }} )' +on: + workflow_dispatch: + inputs: + testnet_type: + description: 'Testnet Type' + required: true + default: 'dev-testnet' + type: choice + options: + - 'dev-testnet' + - 'testnet' + - 'sepolia-testnet' + mgmt_contract_addr: + description: 'Deployed management contract which will be used to request the funds' + required: true + type: string + acc_to_pay: + description: '(Ignored) Address which will receive the funds' + required: false + type: string + +jobs: + recover-network-funds: + runs-on: ubuntu-latest + environment: + name: ${{ github.event.inputs.testnet_type }} + steps: + - uses: actions/checkout@v3 + + - name: 'Login to Azure docker registry' + uses: azure/docker-login@v1 + with: + login-server: testnetobscuronet.azurecr.io + username: testnetobscuronet + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: 'Build and push docker image' + run: | + DOCKER_BUILDKIT=1 docker build -t ${{vars.L2_HARDHATDEPLOYER_DOCKER_BUILD_TAG}} -f tools/hardhatdeployer/Dockerfile . + docker push ${{vars.L2_HARDHATDEPLOYER_DOCKER_BUILD_TAG}} + + - name: 'Deploy L2 contracts' + id: deployL2Contracts + shell: bash + run: | + go run ./testnet/launcher/fundsrecovery/cmd \ + -l1_http_url=${{ secrets.L1_HTTP_URL }} \ + -private_key=${{ secrets.WORKER_PK }} \ + -mgmt_contract_addr=${{ github.event.inputs.mgmt_contract_addr }} \ + -docker_image=${{vars.L2_HARDHATDEPLOYER_DOCKER_BUILD_TAG}} \ + -acc_to_pay=${{ github.event.inputs.acc_to_pay }} + + - name: 'Save container logs on failure' + if: failure() + run: | + docker logs `docker ps -aqf "name=recover-funds"` > recover-funds.out 2>&1 + + - name: 'Upload container logs on failure' + uses: actions/upload-artifact@v3 + if: failure() + with: + name: recover-funds + path: | + recover-funds.out + retention-days: 2 diff --git a/contracts/deployment_scripts/testnet/recoverfunds/001_recover_funds.ts b/contracts/deployment_scripts/testnet/recoverfunds/001_recover_funds.ts new file mode 100644 index 0000000000..91195a52e5 --- /dev/null +++ b/contracts/deployment_scripts/testnet/recoverfunds/001_recover_funds.ts @@ -0,0 +1,25 @@ +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {DeployFunction} from 'hardhat-deploy/types'; + + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const {deployer} = await hre.getNamedAccounts(); + + const mgmtContractAddress = process.env.MGMT_CONTRACT_ADDRESS!! + // todo: if we want to support this we need to add the payAcc address param to the RetrieveAllBridgeFunds solidity defn + const addressToPay = process.env.ACC_TO_PAY!! + + const mgmtContract = (await hre.ethers.getContractFactory('ManagementContract')).attach(mgmtContractAddress) + const tx = await mgmtContract.RetrieveAllBridgeFunds(); + const receipt = await tx.wait(); + + // Check the receipt for success, logs, etc. + if (receipt.status === 1) { + console.log("Successfully recovered funds from the bridge."); + } else { + console.log("Recovery transaction failed"); + } +}; + +export default func; +// No dependencies \ No newline at end of file diff --git a/testnet/launcher/fundsrecovery/cmd/cli.go b/testnet/launcher/fundsrecovery/cmd/cli.go new file mode 100644 index 0000000000..751dcc072d --- /dev/null +++ b/testnet/launcher/fundsrecovery/cmd/cli.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" +) + +// FundsRecoveryConfigCLI represents the configurations passed into the deployer over CLI +type FundsRecoveryConfigCLI struct { + l1HTTPURL string + privateKey string + dockerImage string + mgmtContractAddress string + accToPay string +} + +// ParseConfigCLI returns a NodeConfigCLI based the cli params and defaults. +func ParseConfigCLI() *FundsRecoveryConfigCLI { + cfg := &FundsRecoveryConfigCLI{} + flagUsageMap := getFlagUsageMap() + + l1HTTPURL := flag.String(l1HTTPURLFlag, "", flagUsageMap[l1HTTPURLFlag]) + privateKey := flag.String(privateKeyFlag, "", flagUsageMap[privateKeyFlag]) + dockerImage := flag.String(dockerImageFlag, "", flagUsageMap[dockerImageFlag]) + mgmtContractAddr := flag.String(mgmtContractAddrFlag, "", flagUsageMap[mgmtContractAddrFlag]) + accToPay := flag.String(accToPayFlag, "", flagUsageMap[accToPayFlag]) + + flag.Parse() + + cfg.l1HTTPURL = *l1HTTPURL + cfg.privateKey = *privateKey + cfg.dockerImage = *dockerImage + cfg.mgmtContractAddress = *mgmtContractAddr + cfg.accToPay = *accToPay + + return cfg +} diff --git a/testnet/launcher/fundsrecovery/cmd/cli_flags.go b/testnet/launcher/fundsrecovery/cmd/cli_flags.go new file mode 100644 index 0000000000..f4d84b8d53 --- /dev/null +++ b/testnet/launcher/fundsrecovery/cmd/cli_flags.go @@ -0,0 +1,22 @@ +package main + +// Flag names. +const ( + l1HTTPURLFlag = "l1_http_url" + privateKeyFlag = "private_key" + dockerImageFlag = "docker_image" + mgmtContractAddrFlag = "mgmt_contract_addr" + accToPayFlag = "acc_to_pay" +) + +// Returns a map of the flag usages. +// While we could just use constants instead of a map, this approach allows us to test that all the expected flags are defined. +func getFlagUsageMap() map[string]string { + return map[string]string{ + l1HTTPURLFlag: "Layer 1 network http RPC addr", + privateKeyFlag: "L1 mgmt contract owning key", + dockerImageFlag: "Docker image to run", + mgmtContractAddrFlag: "Address of the management contract", + accToPayFlag: "Address to receive the recovered funds", + } +} diff --git a/testnet/launcher/fundsrecovery/cmd/main.go b/testnet/launcher/fundsrecovery/cmd/main.go new file mode 100644 index 0000000000..9c93e85bc7 --- /dev/null +++ b/testnet/launcher/fundsrecovery/cmd/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + + funds "github.com/obscuronet/go-obscuro/testnet/launcher/fundsrecovery" +) + +func main() { + cliConfig := ParseConfigCLI() + + fundsRecovery, err := funds.NewFundsRecovery( + funds.NewFundsRecoveryConfig( + funds.WithL1HTTPURL(cliConfig.l1HTTPURL), + funds.WithL1PrivateKey(cliConfig.privateKey), + funds.WithMgmtContractAddress(cliConfig.mgmtContractAddress), + funds.WithDockerImage(cliConfig.dockerImage), + funds.WithAccToPay(cliConfig.accToPay), + ), + ) + if err != nil { + fmt.Println("unable to configure the funds recovery - ", err) + os.Exit(1) + } + + err = fundsRecovery.Start() + if err != nil { + fmt.Println("unable to start the funds recovery - ", err) + os.Exit(1) + } + + err = fundsRecovery.WaitForFinish() + if err != nil { + fmt.Println("unexpected error waiting for funds recovery to finish - ", err) + os.Exit(1) + } + fmt.Println("Funds recovery was successful...") + os.Exit(0) +} diff --git a/testnet/launcher/fundsrecovery/config.go b/testnet/launcher/fundsrecovery/config.go new file mode 100644 index 0000000000..c27a1fa514 --- /dev/null +++ b/testnet/launcher/fundsrecovery/config.go @@ -0,0 +1,53 @@ +package fundsrecovery + +// Option is a function that applies configs to a Config Object +type Option = func(c *Config) + +// Config holds the properties that configure the package +type Config struct { + l1HTTPURL string + l1privateKey string + mgmtContractAddress string + dockerImage string + accToPay string +} + +func NewFundsRecoveryConfig(opts ...Option) *Config { + defaultConfig := &Config{} + + for _, opt := range opts { + opt(defaultConfig) + } + + return defaultConfig +} + +func WithL1HTTPURL(s string) Option { + return func(c *Config) { + c.l1HTTPURL = s + } +} + +func WithL1PrivateKey(s string) Option { + return func(c *Config) { + c.l1privateKey = s + } +} + +func WithMgmtContractAddress(s string) Option { + return func(c *Config) { + c.mgmtContractAddress = s + } +} + +func WithDockerImage(s string) Option { + return func(c *Config) { + c.dockerImage = s + } +} + +func WithAccToPay(s string) Option { + return func(c *Config) { + c.accToPay = s + } +} diff --git a/testnet/launcher/fundsrecovery/docker.go b/testnet/launcher/fundsrecovery/docker.go new file mode 100644 index 0000000000..5db43576a8 --- /dev/null +++ b/testnet/launcher/fundsrecovery/docker.go @@ -0,0 +1,105 @@ +package fundsrecovery + +import ( + "bytes" + "context" + "fmt" + "io" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/obscuronet/go-obscuro/go/common/docker" + "github.com/sanity-io/litter" +) + +type FundsRecovery struct { + cfg *Config + containerID string +} + +func NewFundsRecovery(cfg *Config) (*FundsRecovery, error) { + return &FundsRecovery{ + cfg: cfg, + }, nil // todo (@pedro) - add validation +} + +func (n *FundsRecovery) Start() error { + fmt.Printf("Starting L2 contract deployer with config: \n%s\n\n", litter.Sdump(*n.cfg)) + + cmds := []string{ + "npx", "hardhat", "deploy", + "--network", "layer1", + } + + envs := map[string]string{ + "ACC_TO_PAY": n.cfg.accToPay, + "MGMT_CONTRACT_ADDRESS": n.cfg.mgmtContractAddress, + "NETWORK_JSON": fmt.Sprintf(` +{ + "layer1" : { + "url" : "%s", + "live" : false, + "saveDeployments" : true, + "deploy": [ + "deployment_scripts/testnet/recoverfunds" + ], + "accounts": [ + "%s" + ] + } + } +`, n.cfg.l1HTTPURL, n.cfg.l1privateKey), + } + + containerID, err := docker.StartNewContainer("recover-funds", n.cfg.dockerImage, cmds, nil, envs, nil, nil) + if err != nil { + return err + } + n.containerID = containerID + return nil +} + +func (n *FundsRecovery) GetID() string { + return n.containerID +} + +func (n *FundsRecovery) WaitForFinish() error { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + defer cli.Close() + + // make sure the container has finished execution + err = docker.WaitForContainerToFinish(n.containerID, 10*time.Minute) + if err != nil { + n.PrintLogs(cli) + return err + } + + // if we want to read anything from the container logs we can do it here (see RetrieveL1ContractAddresses as example) + + return nil +} + +func (n *FundsRecovery) PrintLogs(cli *client.Client) { + logsOptions := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + + // Read the container logs + out, err := cli.ContainerLogs(context.Background(), n.containerID, logsOptions) + if err != nil { + fmt.Printf("Error printing out container %s logs... %v\n", n.containerID, err) + } + defer out.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, out) + if err != nil { + fmt.Printf("Error getting logs for container %s\n", n.containerID) + } + fmt.Println(buf.String()) +}