From ea8bae1416c2c4fad8a5a30cb03118675a52994e Mon Sep 17 00:00:00 2001 From: noot <36753753+noot@users.noreply.github.com> Date: Fri, 21 Apr 2023 22:37:10 +0200 Subject: [PATCH] feat: add dockerfile (#387) Co-authored-by: Dmitry Holodov --- Makefile | 4 +- cliutil/utils.go | 2 +- cmd/swapd/contract.go | 5 +- cmd/swapd/main.go | 71 ++++++++++++++++------------ docs/default-file-locations.md | 2 +- docs/stagenet.md | 4 +- scripts/docker/Dockerfile | 61 ++++++++++++++++++++++++ scripts/docker/build-docker-image.sh | 19 ++++++++ scripts/docker/docker-entrypoint.sh | 43 +++++++++++++++++ scripts/docker/example-docker-run.sh | 26 ++++++++++ scripts/setup-stagenet.sh | 2 +- 11 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 scripts/docker/Dockerfile create mode 100755 scripts/docker/build-docker-image.sh create mode 100755 scripts/docker/docker-entrypoint.sh create mode 100755 scripts/docker/example-docker-run.sh diff --git a/Makefile b/Makefile index 0481d794d..e6ca7171b 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ lint-go: .PHONY: lint-shell lint-shell: - shellcheck --source-path=.:scripts scripts/*.sh + shellcheck --source-path=.:scripts scripts/*.sh scripts/*/*.sh .PHONY: lint-solidity lint-solidity: @@ -26,7 +26,7 @@ format-go: .PHONY: format-shell format-shell: - shfmt -w scripts/*.sh + shfmt -w scripts/*.sh scripts/*/*.sh .PHONY: format-solidity format-solidity: diff --git a/cliutil/utils.go b/cliutil/utils.go index 805672719..60c385d8f 100644 --- a/cliutil/utils.go +++ b/cliutil/utils.go @@ -76,7 +76,7 @@ func GetEthereumPrivateKey(ethPrivKeyFile string, env common.Environment, devXMR fileData, err := os.ReadFile(filepath.Clean(ethPrivKeyFile)) if err != nil { - return nil, fmt.Errorf("failed to read ethereum-privkey file: %w", err) + return nil, fmt.Errorf("failed to read eth-privkey file: %w", err) } ethPrivKeyHex := strings.TrimSpace(string(fileData)) privkey, err := ethcrypto.HexToECDSA(ethPrivKeyHex) diff --git a/cmd/swapd/contract.go b/cmd/swapd/contract.go index 2de2376d9..39f90d4ea 100644 --- a/cmd/swapd/contract.go +++ b/cmd/swapd/contract.go @@ -6,7 +6,6 @@ package main import ( "context" "crypto/ecdsa" - "errors" "fmt" "os" "path" @@ -26,7 +25,7 @@ const ( ) var ( - errNoEthereumPrivateKey = errors.New("must provide --ethereum-privkey file for non-development environment") + errNoEthPrivateKey = fmt.Errorf("must provide --%s file for non-development environment", flagEthPrivKey) ) type contractAddresses struct { @@ -73,7 +72,7 @@ func deploySwapCreator( ) (ethcommon.Address, *contracts.SwapCreator, error) { if privkey == nil { - return ethcommon.Address{}, nil, errNoEthereumPrivateKey + return ethcommon.Address{}, nil, errNoEthPrivateKey } if (forwarderAddr == ethcommon.Address{}) { diff --git a/cmd/swapd/main.go b/cmd/swapd/main.go index 051cecfd5..1a4ed87ca 100644 --- a/cmd/swapd/main.go +++ b/cmd/swapd/main.go @@ -63,8 +63,8 @@ const ( flagMoneroWalletPath = "wallet-file" flagMoneroWalletPassword = "wallet-password" flagMoneroWalletPort = "wallet-port" - flagEthereumEndpoint = "ethereum-endpoint" - flagEthereumPrivKey = "ethereum-privkey" + flagEthEndpoint = "eth-endpoint" + flagEthPrivKey = "eth-privkey" flagContractAddress = "contract-address" flagGasPrice = "gas-price" flagGasLimit = "gas-limit" @@ -91,9 +91,10 @@ func cliApp() *cli.App { Suggest: true, Flags: []cli.Flag{ &cli.UintFlag{ - Name: flagRPCPort, - Usage: "Port for the daemon RPC server to run on", - Value: defaultRPCPort, + Name: flagRPCPort, + Usage: "Port for the daemon RPC server to run on", + Value: defaultRPCPort, + EnvVars: []string{"SWAPD_RPC_PORT"}, }, &cli.StringFlag{ Name: flagDataDir, @@ -106,25 +107,28 @@ func cliApp() *cli.App { Value: fmt.Sprintf("{DATA_DIR}/%s", common.DefaultLibp2pKeyFileName), }, &cli.UintFlag{ - Name: flagLibp2pPort, - Usage: "libp2p port to listen on", - Value: defaultLibp2pPort, + Name: flagLibp2pPort, + Usage: "libp2p port to listen on", + Value: defaultLibp2pPort, + EnvVars: []string{"SWAPD_LIBP2P_PORT"}, }, &cli.StringFlag{ - Name: flagEnv, - Usage: "Environment to use: one of mainnet, stagenet, or dev", - Value: "dev", + Name: flagEnv, + Usage: "Environment to use: one of mainnet, stagenet, or dev", + EnvVars: []string{"SWAPD_ENV"}, + Value: "dev", }, &cli.StringFlag{ - Name: flagMoneroDaemonHost, - Usage: "monerod host", - Value: "127.0.0.1", + Name: flagMoneroDaemonHost, + Usage: "monerod host", + EnvVars: []string{"SWAPD_MONEROD_HOST"}, }, &cli.UintFlag{ Name: flagMoneroDaemonPort, Usage: fmt.Sprintf("monerod port (--%s=stagenet changes default to %d)", flagEnv, common.DefaultMoneroDaemonStagenetPort), - Value: common.DefaultMoneroDaemonMainnetPort, // at least for now, this is also the dev default + EnvVars: []string{"SWAPD_MONEROD_PORT"}, + Value: common.DefaultMoneroDaemonMainnetPort, // at least for now, this is also the dev default }, &cli.StringFlag{ Name: flagMoneroWalletPath, @@ -141,13 +145,17 @@ func cliApp() *cli.App { Hidden: true, // flag is for integration tests and won't be supported long term }, &cli.StringFlag{ - Name: flagEthereumEndpoint, - Usage: "Ethereum client endpoint", + Name: flagEthEndpoint, + Usage: "Ethereum client endpoint", + Aliases: []string{"ethereum-endpoint"}, + EnvVars: []string{"SWAPD_ETH_ENDPOINT"}, }, &cli.StringFlag{ - Name: flagEthereumPrivKey, - Usage: "File containing ethereum private key as hex, new key is generated if missing", - Value: fmt.Sprintf("{DATA-DIR}/%s", common.DefaultEthKeyFileName), + Name: flagEthPrivKey, + Usage: "File containing ethereum private key as hex, new key is generated if missing", + Aliases: []string{"ethereum-privkey"}, + EnvVars: []string{"SWAPD_ETH_PRIVKEY"}, + Value: fmt.Sprintf("{DATA-DIR}/%s", common.DefaultEthKeyFileName), }, &cli.StringFlag{ Name: flagContractAddress, @@ -188,9 +196,10 @@ func cliApp() *cli.App { Usage: "Leave XMR in generated swap wallet instead of sweeping funds to primary.", }, &cli.StringFlag{ - Name: flagLogLevel, - Usage: "Set log level: one of [error|warn|info|debug]", - Value: "info", + Name: flagLogLevel, + Usage: "Set log level: one of [error|warn|info|debug]", + Value: "info", + EnvVars: []string{"SWAPD_LOG_LEVEL"}, }, &cli.BoolFlag{ Name: flagUseExternalSigner, @@ -414,7 +423,7 @@ func createMoneroClient(c *cli.Context, envConf *common.Config) (monero.WalletCl Env: envConf.Env, WalletFilePath: walletFilePath, MonerodNodes: envConf.MoneroNodes, - MoneroWalletRPCPath: "", // look for it in "monero-bin/monero-wallet-rpc" and then the user's path + MoneroWalletRPCPath: "", // look for it in "./monero-bin/monero-wallet-rpc" and then the user's path WalletPassword: c.String(flagMoneroWalletPassword), WalletPort: c.Uint(flagMoneroWalletPort), }) @@ -424,23 +433,23 @@ func createEthClient(c *cli.Context, envConf *common.Config) (extethclient.EthCl env := envConf.Env ethEndpoint := common.DefaultEthEndpoint - if c.String(flagEthereumEndpoint) != "" { - ethEndpoint = c.String(flagEthereumEndpoint) + if c.String(flagEthEndpoint) != "" { + ethEndpoint = c.String(flagEthEndpoint) } var ethPrivKey *ecdsa.PrivateKey useExternalSigner := c.Bool(flagUseExternalSigner) - if useExternalSigner && c.IsSet(flagEthereumPrivKey) { - return nil, errFlagsMutuallyExclusive(flagUseExternalSigner, flagEthereumPrivKey) + if useExternalSigner && c.IsSet(flagEthPrivKey) { + return nil, errFlagsMutuallyExclusive(flagUseExternalSigner, flagEthPrivKey) } if !useExternalSigner { ethPrivKeyFile := envConf.EthKeyFileName() - if c.IsSet(flagEthereumPrivKey) { - ethPrivKeyFile = c.String(flagEthereumPrivKey) + if c.IsSet(flagEthPrivKey) { + ethPrivKeyFile = c.String(flagEthPrivKey) if ethPrivKeyFile == "" { - return nil, errFlagValueEmpty(flagEthereumPrivKey) + return nil, errFlagValueEmpty(flagEthPrivKey) } } diff --git a/docs/default-file-locations.md b/docs/default-file-locations.md index 6a9b2d878..ad684f919 100644 --- a/docs/default-file-locations.md +++ b/docs/default-file-locations.md @@ -41,7 +41,7 @@ file above. More information on what the individual files contain can be ### {DATA_DIR}/eth.key This is the default location of your Ethereum private key used by swaps. Alternate -locations can be configured with `--ethereum-privkey`. If the file does not +locations can be configured with `--eth-privkey`. If the file does not exist, a new random key will be created and placed in this location. ### {DATA_DIR}/net.key diff --git a/docs/stagenet.md b/docs/stagenet.md index 64d944458..3f9db81fc 100644 --- a/docs/stagenet.md +++ b/docs/stagenet.md @@ -54,9 +54,9 @@ cd atomic-swap make build ``` -10. Start the `swapd` daemon. Change `--ethereum-endpoint` to point to your endpoint. +10. Start the `swapd` daemon. Change `--eth-endpoint` to point to your endpoint. ```bash -./swapd --env stagenet --ethereum-endpoint= +./swapd --env stagenet --eth-endpoint SEPOLIA_ENDPOINT ``` Note: You probably need additional flags above: * `--data-dir PATH`: Needed if you are launching more than one `swapd` instance diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile new file mode 100644 index 000000000..7b1b7ec7d --- /dev/null +++ b/scripts/docker/Dockerfile @@ -0,0 +1,61 @@ +FROM golang:1.20 as builder + +# Download monero-wallet-rpc. We need bzip2 to unpack the tar file. +RUN apt update && apt install -y bzip2 +RUN arch=$(uname -m | sed 's/x86_64/linux64/; s/aarch64/linuxarm8/') && \ + curl -sSL "https://downloads.getmonero.org/cli/${arch}" -o monero.tar.bz2 +RUN tar xvjf monero.tar.bz2 --no-anchored monero-wallet-rpc --strip-components=1 + +# Build the swapd and swapcli binaries. The BRANCH argument can be set to a +# branch, release tag, "latest", or a commit hash. +ARG VERSION=master +RUN go install -tags=prod \ + github.com/athanorlabs/atomic-swap/cmd/swapd@"${VERSION}" \ + github.com/athanorlabs/atomic-swap/cmd/swapcli@"${VERSION}" +RUN /go/bin/swapd --version + +FROM debian:bullseye-slim +RUN apt-get update && apt-get install -y ca-certificates gosu + +# /usr/local/bin has swapd, swapcli, monero-wallet-rpc and +# docker-entrypoint.sh. +COPY --from=builder /go/monero-wallet-rpc /usr/local/bin/ +COPY --from=builder /go/bin/ /usr/local/bin/ +COPY ./docker-entrypoint.sh /usr/local/bin/ + +VOLUME /data + +# USER_UID and USER_GID are defined as ARGs so that, if desired, you can +# build the container with a UID equal to some user outside the container +# that will own the files in /data. +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +RUN groupadd --gid "${USER_GID}" atomic && \ + useradd --no-log-init --home-dir /atomic-swap \ + --uid "${USER_UID}" --gid "${USER_GID}" -m atomic && \ + ln -s /data /atomic-swap/.atomicswap + +# 9900 the default p2p port. swapd also listens to swapcli on 127.0.0.1:5000, +# which is not accessible outside the container by default. You have 2 options +# to interact with this RPC port: +# (1) Use swapcli inside the container:: +# $ docker exec CONTAINER_NAME_OR_ID swapcli SUBCOMMAND ... +# (2) Run the container with --network=host so 127.0.0.1:5000 is the same +# port inside and outside of the container. +EXPOSE 9900/udp +EXPOSE 9900/tcp + +# The swapd environment (dev, stagenet, mainnet) can be convigured via the +# SWAPD_ENV environment variable or using swapd's --env flag (which takes +# precidence). In docker, we use the environment variable to configure file +# permissions of the correct directory in the data volume. The suggested +# ways of working with this are: +# (1) Set SWAPD_ENV variable and don't use the CLI flag +# (2) Use swapd's --env=ENVIRONMENT CLI flag, but set SWAPD_ENV to the +# identical environment or the empty string. +ENV SWAPD_ENV=stagenet +ENV SWAPD_ETH_ENDPOINT=https://rpc.sepolia.org/ +ENV SWAPD_LOG_LEVEL=info + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["swapd"] diff --git a/scripts/docker/build-docker-image.sh b/scripts/docker/build-docker-image.sh new file mode 100755 index 000000000..413420005 --- /dev/null +++ b/scripts/docker/build-docker-image.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e + +IMAGE_NAME=atomic-swap + +# VERSION can be "latest", a release tag, a hash or a branch name that does not contain slashes. +# It must exist on github, local changes are not visible inside the container. +VERSION=master + +# Run docker build from the directory of this script +cd "$(dirname "$0")" + +docker build \ + --build-arg "VERSION=${VERSION}" \ + --build-arg "USER_UID=$(id -u)" \ + --build-arg "USER_GID=$(id -g)" \ + . -t "${IMAGE_NAME}:${VERSION}" + +echo "built ${IMAGE_NAME}:${VERSION}" diff --git a/scripts/docker/docker-entrypoint.sh b/scripts/docker/docker-entrypoint.sh new file mode 100755 index 000000000..e58fe33cc --- /dev/null +++ b/scripts/docker/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +cmd="$(basename "${1}")" + +# +# If we are running swapd and SWAPD_ENV is set, so this script +# knows where swapd will be writing data, we ensure that the +# atomic user that runs swapd has access the directories where +# the data is written. +# +if [[ "${cmd}" == 'swapd' ]] && [[ -n "${SWAPD_ENV}" ]]; then + + if ! [[ "${SWAPD_ENV}" =~ ^dev|stagenet|mainnet$ ]]; then + echo "invalid SWAPD_ENV value" + exit 1 + fi + + if [[ "${*}:1}" =~ '--data-dir' ]]; then + echo "Setting --data-dir is not recommended for dockerized swapd." + echo "If required, unset SWAPD_ENV or override the entrypoint." + exit 1 + fi + + data_dir="/data/${SWAPD_ENV}" + + # create the directory if it does not exist + if [[ ! -d "${data_dir}" ]]; then + mkdir --mode=700 "${data_dir}" + fi + + # ensure the files are owned by the atomic user + chown -R atomic.atomic "${data_dir}" +fi + +# Run swapd and swapcli commands as the atomic user for reduced +# privileges. +if [[ "${cmd}" == 'swapd' || "${cmd}" == 'swacli' ]]; then + exec gosu atomic "$@" +fi + +exec "$@" diff --git a/scripts/docker/example-docker-run.sh b/scripts/docker/example-docker-run.sh new file mode 100755 index 000000000..e32d9f10c --- /dev/null +++ b/scripts/docker/example-docker-run.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -ex + +CONTAINER_NAME=atomic-stagenet +IMAGE_NAME=atomic-swap +TAG=master + +# Setting NETWORK to "host" allows you to run swapcli commands on the local +# host. You can also use "bridge", which requires all swapcli commands to +# be run from inside the container. +NETWORK=host + +# Note: We mount one directory above what swapd considers its "data-dir". +DATA_MOUNT_DIR="${HOME}/.atomicswap/docker" + +# Pre-create the mounted directory, or docker will create it with root +# as the owner. +mkdir -p "${DATA_MOUNT_DIR}" + +docker run --rm -v "${DATA_MOUNT_DIR}:/data" \ + --env SWAPD_ENV=stagenet \ + --env SWAPD_ETH_ENDPOINT="https://rpc.sepolia.org/" \ + --env SWAPD_LOG_LEVEL=debug \ + --network="${NETWORK}" \ + --name="${CONTAINER_NAME}" \ + "${IMAGE_NAME}:${TAG}" diff --git a/scripts/setup-stagenet.sh b/scripts/setup-stagenet.sh index c7ecd3307..48a22fea8 100755 --- a/scripts/setup-stagenet.sh +++ b/scripts/setup-stagenet.sh @@ -21,7 +21,7 @@ log_level=info # change to "debug" for more logs ./bin/swapd --env stagenet \ "--log-level=${log_level}" \ - "--ethereum-endpoint=${ETHEREUM_ENDPOINT}" \ + "--eth-endpoint=${ETHEREUM_ENDPOINT}" \ &>swapd.log & echo "swapd start with logs in swapd.log"