diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68ebfadf974..9c6ed312321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,8 +265,8 @@ jobs: - name: Check image build shell: bash run: bash -x scripts/tests.build_image.sh - test_build_antithesis_avalanchego_image: - name: Antithesis avalanchego build + test_build_antithesis_avalanchego_images: + name: Build Antithesis avalanchego images runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -275,6 +275,16 @@ jobs: run: bash -x scripts/tests.build_antithesis_images.sh env: TEST_SETUP: avalanchego + test_build_antithesis_xsvm_images: + name: Build Antithesis xsvm images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check image build for xsvm test setup + shell: bash + run: bash -x scripts/tests.build_antithesis_images.sh + env: + TEST_SETUP: xsvm govulncheck: runs-on: ubuntu-latest name: govulncheck diff --git a/.github/workflows/publish_antithesis_images.yml b/.github/workflows/publish_antithesis_images.yml index f3b121dd064..35e77218fdb 100644 --- a/.github/workflows/publish_antithesis_images.yml +++ b/.github/workflows/publish_antithesis_images.yml @@ -31,3 +31,10 @@ jobs: IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }} TAG: latest TEST_SETUP: avalanchego + + - name: Build and push images for xsvm test setup + run: bash -x ./scripts/build_antithesis_images.sh + env: + IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }} + TAG: latest + TEST_SETUP: xsvm diff --git a/scripts/build_antithesis_images.sh b/scripts/build_antithesis_images.sh index 3ccccd1d352..8e3c534bbcb 100755 --- a/scripts/build_antithesis_images.sh +++ b/scripts/build_antithesis_images.sh @@ -5,8 +5,10 @@ set -euo pipefail # Builds docker images for antithesis testing. # e.g., -# ./scripts/build_antithesis_images.sh # Build local images -# IMAGE_PREFIX=/ TAG=latest ./scripts/build_antithesis_images.sh # Specify a prefix to enable image push and use a specific tag +# TEST_SETUP=avalanchego ./scripts/build_antithesis_images.sh # Build local images for avalanchego +# TEST_SETUP=avalanchego NODE_ONLY=1 ./scripts/build_antithesis_images.sh # Build only a local node image for avalanchego +# TEST_SETUP=xsvm ./scripts/build_antithesis_images.sh # Build local images for xsvm +# TEST_SETUP=xsvm IMAGE_PREFIX=/ TAG=latest ./scripts/build_antithesis_images.sh # Specify a prefix to enable image push and use a specific tag # Directory above this script AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) @@ -28,11 +30,13 @@ GO_VERSION="$(go list -m -f '{{.GoVersion}}')" function build_images { local test_setup=$1 local uninstrumented_node_dockerfile=$2 + local image_prefix=$3 + local node_only=${4:-} # Define image names local base_image_name="antithesis-${test_setup}" - if [[ -n "${IMAGE_PREFIX}" ]]; then - base_image_name="${IMAGE_PREFIX}/${base_image_name}" + if [[ -n "${image_prefix}" ]]; then + base_image_name="${image_prefix}/${base_image_name}" fi local node_image_name="${base_image_name}-node:${TAG}" local workload_image_name="${base_image_name}-workload:${TAG}" @@ -49,22 +53,65 @@ function build_images { fi # Define default build command - local docker_cmd="docker buildx build --build-arg GO_VERSION=${GO_VERSION}" - if [[ -n "${IMAGE_PREFIX}" ]]; then + local docker_cmd="docker buildx build --build-arg GO_VERSION=${GO_VERSION} --build-arg NODE_IMAGE=${node_image_name}" + + if [[ "${test_setup}" == "xsvm" ]]; then + # The xsvm node image is built on the avalanchego node image, which is assumed to have already been + # built. The image name doesn't include the image prefix because it is not intended to be pushed. + docker_cmd="${docker_cmd} --build-arg AVALANCHEGO_NODE_IMAGE=antithesis-avalanchego-node:${TAG}" + fi + + # Build node image first to allow the workload image to use it. + ${docker_cmd} -t "${node_image_name}" -f "${node_dockerfile}" "${AVALANCHE_PATH}" + if [[ -n "${image_prefix}" ]]; then # Push images with an image prefix since the prefix defines a registry location docker_cmd="${docker_cmd} --push" fi - # Build node image first to allow the config and workload image builds to use it. - ${docker_cmd} -t "${node_image_name}" -f "${node_dockerfile}" "${AVALANCHE_PATH}" - ${docker_cmd} --build-arg NODE_IMAGE="${node_image_name}" -t "${workload_image_name}" -f "${base_dockerfile}.workload" "${AVALANCHE_PATH}" - ${docker_cmd} --build-arg IMAGE_TAG="${TAG}" -t "${config_image_name}" -f "${base_dockerfile}.config" "${AVALANCHE_PATH}" + if [[ -n "${node_only}" ]]; then + # Skip building the config and workload images. Supports building the avalanchego + # node image as the base image for the xsvm node image. + return + fi + + TARGET_PATH="${AVALANCHE_PATH}/build/antithesis/${test_setup}" + if [[ -d "${TARGET_PATH}" ]]; then + # Ensure the target path is empty before generating the compose config + rm -r "${TARGET_PATH:?}" + fi + + # Define the env vars for the compose config generation + COMPOSE_ENV="TARGET_PATH=${TARGET_PATH} IMAGE_TAG=${TAG}" + + if [[ "${test_setup}" == "xsvm" ]]; then + # Ensure avalanchego and xsvm binaries are available to create an initial db state that includes subnets. + "${AVALANCHE_PATH}"/scripts/build.sh + "${AVALANCHE_PATH}"/scripts/build_xsvm.sh + COMPOSE_ENV="${COMPOSE_ENV} AVALANCHEGO_PATH=${AVALANCHE_PATH}/build/avalanchego AVALANCHEGO_PLUGIN_DIR=${HOME}/.avalanchego/plugins" + fi + + # Generate compose config for copying into the config image + # shellcheck disable=SC2086 + env ${COMPOSE_ENV} go run "${AVALANCHE_PATH}/tests/antithesis/${test_setup}/gencomposeconfig" + + # Build the config image + ${docker_cmd} -t "${config_image_name}" -f "${base_dockerfile}.config" "${AVALANCHE_PATH}" + + # Build the workload image + ${docker_cmd} -t "${workload_image_name}" -f "${base_dockerfile}.workload" "${AVALANCHE_PATH}" } TEST_SETUP="${TEST_SETUP:-}" if [[ "${TEST_SETUP}" == "avalanchego" ]]; then - build_images avalanchego "${AVALANCHE_PATH}/Dockerfile" + build_images avalanchego "${AVALANCHE_PATH}/Dockerfile" "${IMAGE_PREFIX}" "${NODE_ONLY:-}" +elif [[ "${TEST_SETUP}" == "xsvm" ]]; then + # Only build the node image to use as the base for the xsvm image. Provide an empty + # image prefix (the 3rd argument) to prevent the image from being pushed + NODE_ONLY=1 + build_images avalanchego "${AVALANCHE_PATH}/Dockerfile" "" "${NODE_ONLY}" + + build_images xsvm "${AVALANCHE_PATH}/vms/example/xsvm/Dockerfile" "${IMAGE_PREFIX}" else - echo "TEST_SETUP must be set. Valid values are 'avalanchego'" + echo "TEST_SETUP must be set. Valid values are 'avalanchego' or 'xsvm'" exit 255 fi diff --git a/scripts/build_antithesis_xsvm_workload.sh b/scripts/build_antithesis_xsvm_workload.sh new file mode 100755 index 00000000000..153965eeb63 --- /dev/null +++ b/scripts/build_antithesis_xsvm_workload.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Directory above this script +AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) +# Load the constants +source "$AVALANCHE_PATH"/scripts/constants.sh + +echo "Building Workload..." +go build -o "$AVALANCHE_PATH/build/antithesis-xsvm-workload" "$AVALANCHE_PATH/tests/antithesis/xsvm/"*.go diff --git a/scripts/tests.build_antithesis_images.sh b/scripts/tests.build_antithesis_images.sh index a10e5c4ccda..8fdce84ab64 100755 --- a/scripts/tests.build_antithesis_images.sh +++ b/scripts/tests.build_antithesis_images.sh @@ -10,6 +10,10 @@ set -euo pipefail # 4. Stopping the workload and its target network # +# e.g., +# TEST_SETUP=avalanchego ./scripts/tests.build_antithesis_images.sh # Test build of images for avalanchego test setup +# DEBUG=1 TEST_SETUP=avalanchego ./scripts/tests.build_antithesis_images.sh # Retain the temporary compose path for troubleshooting + AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd ) # Discover the default tag that will be used for the image @@ -27,6 +31,8 @@ docker create --name "${CONTAINER_NAME}" "${IMAGE_NAME}:${TAG}" /bin/true # Create a temporary directory to write the compose configuration to TMPDIR="$(mktemp -d)" +echo "using temporary directory ${TMPDIR} as the docker-compose path" + COMPOSE_FILE="${TMPDIR}/docker-compose.yml" COMPOSE_CMD="docker-compose -f ${COMPOSE_FILE}" @@ -36,8 +42,10 @@ function cleanup { docker rm "${CONTAINER_NAME}" echo "stopping and removing the docker compose project" ${COMPOSE_CMD} down --volumes - echo "removing temporary dir" - rm -rf "${TMPDIR}" + if [[ -z "${DEBUG:-}" ]]; then + echo "removing temporary dir" + rm -rf "${TMPDIR}" + fi } trap cleanup EXIT @@ -47,9 +55,10 @@ docker cp "${CONTAINER_NAME}":/docker-compose.yml "${COMPOSE_FILE}" # Copy the volume paths out of the container docker cp "${CONTAINER_NAME}":/volumes "${TMPDIR}/" -# Run the docker compose project for one minute without error +# Run the docker compose project for 30 seconds without error. Local +# network bootstrap is ~6s, but github workers can be much slower. ${COMPOSE_CMD} up -d -sleep 60 +sleep 30 if ${COMPOSE_CMD} ps -q | xargs docker inspect -f '{{ .State.Status }}' | grep -v 'running'; then echo "An error occurred." exit 255 diff --git a/tests/antithesis/README.md b/tests/antithesis/README.md index 96328d211d3..c838b64780a 100644 --- a/tests/antithesis/README.md +++ b/tests/antithesis/README.md @@ -8,11 +8,14 @@ enables discovery and reproduction of anomalous behavior. ## Package details -| Filename | Purpose | -|:-------------|:----------------------------------------------------------------------------------| -| compose.go | Enables generation of Docker Compose project files for antithesis testing. | -| avalanchego/ | Contains resources supporting antithesis testing of avalanchego's primary chains. | - +| Filename | Purpose | +|:---------------|:-----------------------------------------------------------------------------------| +| compose.go | Generates Docker Compose project file and initial database for antithesis testing. | +| config.go | Defines common flags for the workload binary. | +| init_db.go | Initializes initial db state for subnet testing. | +| node_health.go | Helper to check node health. | +| avalanchego/ | Defines an antithesis test setup for avalanchego's primary chains. | +| xsvm/ | Defines an antithesis test setup for the xsvm VM. | ## Instrumentation @@ -45,3 +48,55 @@ a test setup: In addition, github workflows are suggested to ensure `scripts/tests.build_antithesis_images.sh` runs against PRs and `scripts/build_antithesis_images.sh` runs against pushes. + +## Troubleshooting a test setup + +### Running a workload directly + +The workload of the 'avalanchego' test setup can be invoked against an +arbitrary network: + +```bash +$ AVAWL_URIS="http://10.0.20.3:9650 http://10.0.20.4:9650" go run ./tests/antithesis/avalanchego +``` + +The workload of a subnet test setup like 'xsvm' additionally requires +a network with a configured chain for the xsvm VM and the ID for that +chain needs to be provided to the workload: + +```bash +$ AVAWL_URIS=... CHAIN_IDS="2S9ypz...AzMj9" go run ./tests/antithesis/xsvm +``` + +### Running a workload with docker-compose + +Running the test script for a given test setup with the `DEBUG` flag +set will avoid cleaning up the the temporary directory where the +docker-compose setup is written to. This will allow manual invocation of +docker-compose to see the log output of the workload. + +```bash +$ DEBUG=1 ./scripts/tests.build_antithesis_images.sh +``` + +After the test script has terminated, the name of the temporary +directory will appear in the output of the script: + +``` +... +using temporary directory /tmp/tmp.E6eHdDr4ln as the docker-compose path" +... +``` + +Running compose from the temporary directory will ensure the workload +output appears on stdout for inspection: + +```bash +$ cd [temporary directory] + +# Start the compose project +$ docker-compose up + +# Cleanup the compose project +$ docker-compose down --volumes +``` diff --git a/tests/antithesis/avalanchego/Dockerfile.config b/tests/antithesis/avalanchego/Dockerfile.config index 5c8236c4bdc..36e0214bb80 100644 --- a/tests/antithesis/avalanchego/Dockerfile.config +++ b/tests/antithesis/avalanchego/Dockerfile.config @@ -1,29 +1,5 @@ -# The version is supplied as a build argument rather than hard-coded -# to minimize the cost of version changes. -ARG GO_VERSION - -# ============= Compilation Stage ================ -FROM golang:$GO_VERSION-bullseye AS builder - -WORKDIR /build -# Copy and download avalanche dependencies using go mod -COPY go.mod . -COPY go.sum . -RUN go mod download - -# Copy the code into the container -COPY . . - -# IMAGE_TAG should be set to the tag for the images in the generated -# docker compose file. -ARG IMAGE_TAG=latest - -# Generate docker compose configuration -RUN TARGET_PATH=./build IMAGE_TAG="$IMAGE_TAG" go run ./tests/antithesis/avalanchego/gencomposeconfig - -# ============= Cleanup Stage ================ FROM scratch AS execution -# Copy the docker compose file and volumes into the container -COPY --from=builder /build/build/docker-compose.yml /docker-compose.yml -COPY --from=builder /build/build/volumes /volumes +# Copy config artifacts from the build path. For simplicity, artifacts +# are built outside of the docker image. +COPY ./build/antithesis/avalanchego/ / diff --git a/tests/antithesis/avalanchego/Dockerfile.node b/tests/antithesis/avalanchego/Dockerfile.node index d4591250e20..6dc1cb782ce 100644 --- a/tests/antithesis/avalanchego/Dockerfile.node +++ b/tests/antithesis/avalanchego/Dockerfile.node @@ -56,6 +56,9 @@ RUN mkdir -p /symbols COPY --from=builder /avalanchego_instrumented/symbols /symbols COPY --from=builder /opt/antithesis/lib/libvoidstar.so /usr/lib/libvoidstar.so +# Use the same path as the uninstrumented node image for consistency +WORKDIR /avalanchego/build + # Copy the executable into the container COPY --from=builder /avalanchego_instrumented/customer/build/avalanchego ./avalanchego diff --git a/tests/antithesis/avalanchego/main.go b/tests/antithesis/avalanchego/main.go index 5d7d614f75c..57b12a51b6c 100644 --- a/tests/antithesis/avalanchego/main.go +++ b/tests/antithesis/avalanchego/main.go @@ -11,11 +11,11 @@ import ( "os" "time" - "github.com/ava-labs/avalanchego/api/health" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/tests/antithesis" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/set" @@ -38,13 +38,15 @@ import ( const NumKeys = 5 func main() { - c, err := NewConfig(os.Args) + c, err := antithesis.NewConfig(os.Args) if err != nil { log.Fatalf("invalid config: %s", err) } ctx := context.Background() - awaitHealthyNodes(ctx, c.URIs) + if err := antithesis.AwaitHealthyNodes(ctx, c.URIs); err != nil { + log.Fatalf("failed to await healthy nodes: %s", err) + } kc := secp256k1fx.NewKeychain(genesis.EWOQKey) walletSyncStartTime := time.Now() @@ -99,8 +101,7 @@ func main() { }, }}) if err != nil { - log.Printf("failed to issue initial funding X-chain baseTx: %s", err) - return + log.Fatalf("failed to issue initial funding X-chain baseTx: %s", err) } log.Printf("issued initial funding X-chain baseTx %s in %s", baseTx.ID(), time.Since(baseStartTime)) @@ -133,39 +134,6 @@ func main() { genesisWorkload.run(ctx) } -func awaitHealthyNodes(ctx context.Context, uris []string) { - for _, uri := range uris { - awaitHealthyNode(ctx, uri) - } - log.Println("all nodes reported healthy") -} - -func awaitHealthyNode(ctx context.Context, uri string) { - client := health.NewClient(uri) - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - - log.Printf("awaiting node health at %s", uri) - for { - res, err := client.Health(ctx, nil) - switch { - case err != nil: - log.Printf("node couldn't be reached at %s", uri) - case res.Healthy: - log.Printf("node reported healthy at %s", uri) - return - default: - log.Printf("node reported unhealthy at %s", uri) - } - - select { - case <-ticker.C: - case <-ctx.Done(): - log.Printf("node health check cancelled at %s", uri) - } - } -} - type workload struct { id int wallet primary.Wallet diff --git a/tests/antithesis/compose.go b/tests/antithesis/compose.go index 3a2a49cb8fd..e17e189281b 100644 --- a/tests/antithesis/compose.go +++ b/tests/antithesis/compose.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "github.com/compose-spec/compose-go/types" @@ -19,6 +20,8 @@ import ( "github.com/ava-labs/avalanchego/utils/perms" ) +const bootstrapIndex = 0 + // Initialize the given path with the docker-compose configuration (compose file and // volumes) needed for an Antithesis test setup. func GenerateComposeConfig( @@ -94,7 +97,6 @@ func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadIm env := types.Mapping{ config.NetworkNameKey: constants.LocalName, - config.AdminAPIEnabledKey: "true", config.LogLevelKey: logging.Debug.String(), config.LogDisplayLevelKey: logging.Trace.String(), config.HTTPHostKey: "0.0.0.0", @@ -104,13 +106,48 @@ func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadIm config.StakingSignerKeyContentKey: signerKey, } - nodeName := "avalanche" + // Apply configuration appropriate to a test network + for k, v := range tmpnet.DefaultTestFlags() { + switch value := v.(type) { + case string: + env[k] = value + case bool: + env[k] = strconv.FormatBool(value) + default: + return nil, fmt.Errorf("unable to convert unsupported type %T to string", v) + } + } + + serviceName := getServiceName(i) + + volumes := []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeBind, + Source: fmt.Sprintf("./volumes/%s/logs", serviceName), + Target: "/root/.avalanchego/logs", + }, + } + + trackSubnets, err := node.Flags.GetStringVal(config.TrackSubnetsKey) + if err != nil { + return nil, err + } + if len(trackSubnets) > 0 { + env[config.TrackSubnetsKey] = trackSubnets + if i == bootstrapIndex { + // DB volume for bootstrap node will need to initialized with the subnet + volumes = append(volumes, types.ServiceVolumeConfig{ + Type: types.VolumeTypeBind, + Source: fmt.Sprintf("./volumes/%s/db", serviceName), + Target: "/root/.avalanchego/db", + }) + } + } + if i == 0 { - nodeName += "-bootstrap-node" bootstrapIP = address + ":9651" bootstrapIDs = node.NodeID.String() } else { - nodeName = fmt.Sprintf("%s-node-%d", nodeName, i+1) env[config.BootstrapIPsKey] = bootstrapIP env[config.BootstrapIDsKey] = bootstrapIDs } @@ -120,18 +157,12 @@ func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadIm env = keyMapToEnvVarMap(env) services[i+1] = types.ServiceConfig{ - Name: nodeName, - ContainerName: nodeName, - Hostname: nodeName, + Name: serviceName, + ContainerName: serviceName, + Hostname: serviceName, Image: nodeImageName, - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeBind, - Source: fmt.Sprintf("./volumes/%s/logs", nodeName), - Target: "/root/.avalanchego/logs", - }, - }, - Environment: env.ToMappingWithEquals(), + Volumes: volumes, + Environment: env.ToMappingWithEquals(), Networks: map[string]*types.ServiceNetworkConfig{ networkName: { Ipv4Address: address, @@ -146,6 +177,15 @@ func newComposeProject(network *tmpnet.Network, nodeImageName string, workloadIm workloadEnv := types.Mapping{ "AVAWL_URIS": strings.Join(uris, " "), } + chainIDs := []string{} + for _, subnet := range network.Subnets { + for _, chain := range subnet.Chains { + chainIDs = append(chainIDs, chain.ChainID.String()) + } + } + if len(chainIDs) > 0 { + workloadEnv["AVAWL_CHAIN_IDS"] = strings.Join(chainIDs, " ") + } workloadName := "workload" services[0] = types.ServiceConfig{ @@ -188,3 +228,14 @@ func keyMapToEnvVarMap(keyMap types.Mapping) types.Mapping { } return envVarMap } + +// Retrieve the service name for a node at the given index. Common to +// GenerateComposeConfig and InitDBVolumes to ensure consistency +// between db volumes configuration and volume paths. +func getServiceName(index int) string { + baseName := "avalanche" + if index == 0 { + return baseName + "-bootstrap-node" + } + return fmt.Sprintf("%s-node-%d", baseName, index) +} diff --git a/tests/antithesis/avalanchego/config.go b/tests/antithesis/config.go similarity index 83% rename from tests/antithesis/avalanchego/config.go rename to tests/antithesis/config.go index 2db1af5fa06..471b12bb2c1 100644 --- a/tests/antithesis/avalanchego/config.go +++ b/tests/antithesis/config.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package main +package antithesis import ( "errors" @@ -15,7 +15,8 @@ import ( ) const ( - URIsKey = "uris" + URIsKey = "uris" + ChainIDsKey = "chain-ids" FlagsName = "workload" EnvPrefix = "avawl" @@ -27,7 +28,8 @@ var ( ) type Config struct { - URIs []string + URIs []string + ChainIDs []string } func NewConfig(arguments []string) (*Config, error) { @@ -37,7 +39,8 @@ func NewConfig(arguments []string) (*Config, error) { } c := &Config{ - URIs: v.GetStringSlice(URIsKey), + URIs: v.GetStringSlice(URIsKey), + ChainIDs: v.GetStringSlice(ChainIDsKey), } return c, c.Verify() } @@ -56,6 +59,7 @@ func parseFlags(arguments []string) (*viper.Viper, error) { fs := pflag.NewFlagSet(FlagsName, pflag.ContinueOnError) fs.StringSlice(URIsKey, []string{primary.LocalAPIURI}, "URIs of nodes that the workload can communicate with") + fs.StringSlice(ChainIDsKey, []string{}, "IDs of chains to target for testing") if err := fs.Parse(arguments[1:]); err != nil { return nil, fmt.Errorf("failed parsing CLI flags: %w", err) } diff --git a/tests/antithesis/init_db.go b/tests/antithesis/init_db.go new file mode 100644 index 00000000000..cad82f623b1 --- /dev/null +++ b/tests/antithesis/init_db.go @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package antithesis + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/perms" +) + +// Given a path, compose the expected path of the bootstrap node's docker compose db volume. +func GetBootstrapVolumePath(targetPath string) (string, error) { + absPath, err := filepath.Abs(targetPath) + if err != nil { + return "", fmt.Errorf("failed to convert target path to absolute path: %w", err) + } + return filepath.Join(absPath, "volumes", getServiceName(bootstrapIndex)), nil +} + +// Bootstraps a local process-based network, creates its subnets and chains, and copies +// the resulting db state from one of the nodes to the provided path. The path will be +// created if it does not already exist. +func InitBootstrapDB(network *tmpnet.Network, avalancheGoPath string, pluginDir string, destPath string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) + defer cancel() + if err := tmpnet.StartNewNetwork( + ctx, + os.Stdout, + network, + "", + avalancheGoPath, + pluginDir, + ); err != nil { + return fmt.Errorf("failed to start network: %w", err) + } + // Since the goal is to initialize the DB, we can stop the network after it has been started successfully + if err := network.Stop(ctx); err != nil { + return fmt.Errorf("failed to stop network: %w", err) + } + + // Copy the db state from the bootstrap node to the compose volume path. + sourcePath := filepath.Join(network.Nodes[0].GetDataDir(), "db") + if err := os.MkdirAll(destPath, perms.ReadWriteExecute); err != nil { + return fmt.Errorf("failed to create db path %q: %w", destPath, err) + } + // TODO(marun) Replace with os.CopyFS once we upgrade to Go 1.23 + cmd := exec.Command("cp", "-r", sourcePath, destPath) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to copy bootstrap db from %q to %q: %w", sourcePath, destPath, err) + } + + return nil +} diff --git a/tests/antithesis/node_health.go b/tests/antithesis/node_health.go new file mode 100644 index 00000000000..039442398a7 --- /dev/null +++ b/tests/antithesis/node_health.go @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package antithesis + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/ava-labs/avalanchego/api/health" +) + +// Waits for the nodes at the provided URIs to report healthy. +func AwaitHealthyNodes(ctx context.Context, uris []string) error { + for _, uri := range uris { + if err := awaitHealthyNode(ctx, uri); err != nil { + return err + } + } + log.Println("all nodes reported healthy") + return nil +} + +func awaitHealthyNode(ctx context.Context, uri string) error { + client := health.NewClient(uri) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + log.Printf("awaiting node health at %s", uri) + for { + res, err := client.Health(ctx, nil) + switch { + case err != nil: + log.Printf("node couldn't be reached at %s", uri) + case res.Healthy: + log.Printf("node reported healthy at %s", uri) + return nil + default: + log.Printf("node reported unhealthy at %s", uri) + } + + select { + case <-ticker.C: + case <-ctx.Done(): + return fmt.Errorf("node health check cancelled at %s: %w", uri, ctx.Err()) + } + } +} diff --git a/tests/antithesis/xsvm/Dockerfile.config b/tests/antithesis/xsvm/Dockerfile.config new file mode 100644 index 00000000000..3c1128c6f51 --- /dev/null +++ b/tests/antithesis/xsvm/Dockerfile.config @@ -0,0 +1,5 @@ +FROM scratch AS execution + +# Copy config artifacts from the build path. For simplicity, artifacts +# are built outside of the docker image. +COPY ./build/antithesis/xsvm/ / diff --git a/tests/antithesis/xsvm/Dockerfile.node b/tests/antithesis/xsvm/Dockerfile.node new file mode 100644 index 00000000000..1d8d673026d --- /dev/null +++ b/tests/antithesis/xsvm/Dockerfile.node @@ -0,0 +1,61 @@ +# The version is supplied as a build argument rather than hard-coded +# to minimize the cost of version changes. +ARG GO_VERSION + +# AVALANCHEGO_NODE_IMAGE needs to identify an existing avalanchego node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE + +# Antithesis: Getting the Antithesis golang instrumentation library +FROM docker.io/antithesishq/go-instrumentor AS instrumentor + +# ============= Compilation Stage ================ +FROM golang:$GO_VERSION-bullseye AS builder + +WORKDIR /build +# Copy and download avalanche dependencies using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the code into the container +COPY . . + +# Keep the commit hash to easily verify the exact version that is running +RUN git rev-parse HEAD > ./commit_hash.txt + +# Copy the instrumentor and supporting files to their correct locations +COPY --from=instrumentor /opt/antithesis /opt/antithesis +COPY --from=instrumentor /opt/antithesis/lib /lib + +# Create the destination output directory for the instrumented code +RUN mkdir -p /avalanchego_instrumented + +# Park the .git file in a safe location +RUN mkdir -p /opt/tmp/ +RUN cp -r .git /opt/tmp/ + +# Instrument avalanchego +RUN /opt/antithesis/bin/goinstrumentor \ + -stderrthreshold=INFO \ + -antithesis /opt/antithesis/instrumentation \ + . \ + /avalanchego_instrumented + +WORKDIR /avalanchego_instrumented/customer +RUN go mod download +RUN ln -s /opt/tmp/.git .git + +# Build xsvm VM +RUN ./scripts/build_xsvm.sh + +# ============= Cleanup Stage ================ +FROM $AVALANCHEGO_NODE_IMAGE AS execution + +# The commit hash and antithesis dependencies should be part of the base image. + +# Copy the executable into the container +RUN mkdir -p /root/.avalanchego/plugins +COPY --from=builder /avalanchego_instrumented/customer/build/xsvm \ + /root/.avalanchego/plugins/v3m4wPxaHpvGr8qfMeyK6PRW3idZrPHmYcMTt7oXdK47yurVH + +# The node image's entrypoint will be reused. diff --git a/tests/antithesis/xsvm/Dockerfile.workload b/tests/antithesis/xsvm/Dockerfile.workload new file mode 100644 index 00000000000..f9da9009fb0 --- /dev/null +++ b/tests/antithesis/xsvm/Dockerfile.workload @@ -0,0 +1,30 @@ +# The version is supplied as a build argument rather than hard-coded +# to minimize the cost of version changes. +ARG GO_VERSION + +# NODE_IMAGE needs to identify an existing node image and should include the tag +ARG NODE_IMAGE + +# ============= Compilation Stage ================ +FROM golang:$GO_VERSION-bullseye AS builder + +WORKDIR /build +# Copy and download avalanche dependencies using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the code into the container +COPY . . + +# Build the workload +RUN ./scripts/build_antithesis_xsvm_workload.sh + +# ============= Cleanup Stage ================ +# Base the workflow on the node image to support bootstrap testing +FROM $NODE_IMAGE AS execution + +# Copy the executable into the container +COPY --from=builder /build/build/antithesis-xsvm-workload ./workload + +CMD [ "./workload" ] diff --git a/tests/antithesis/xsvm/gencomposeconfig/main.go b/tests/antithesis/xsvm/gencomposeconfig/main.go new file mode 100644 index 00000000000..43720d56155 --- /dev/null +++ b/tests/antithesis/xsvm/gencomposeconfig/main.go @@ -0,0 +1,62 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "fmt" + "log" + "os" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/tests/antithesis" + "github.com/ava-labs/avalanchego/tests/fixture/subnet" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" +) + +const baseImageName = "antithesis-xsvm" + +// Creates docker-compose.yml and its associated volumes in the target path. +func main() { + avalancheGoPath := os.Getenv("AVALANCHEGO_PATH") + if len(avalancheGoPath) == 0 { + log.Fatal("AVALANCHEGO_PATH environment variable not set") + } + + pluginDir := os.Getenv("AVALANCHEGO_PLUGIN_DIR") + if len(pluginDir) == 0 { + log.Fatal("AVALANCHEGO_PLUGIN_DIR environment variable not set") + } + + targetPath := os.Getenv("TARGET_PATH") + if len(targetPath) == 0 { + log.Fatal("TARGET_PATH environment variable not set") + } + + imageTag := os.Getenv("IMAGE_TAG") + if len(imageTag) == 0 { + log.Fatal("IMAGE_TAG environment variable not set") + } + + nodeImageName := fmt.Sprintf("%s-node:%s", baseImageName, imageTag) + workloadImageName := fmt.Sprintf("%s-workload:%s", baseImageName, imageTag) + + // Create a network with an xsvm subnet + network := tmpnet.LocalNetworkOrPanic() + network.Subnets = []*tmpnet.Subnet{ + subnet.NewXSVMOrPanic("xsvm", genesis.VMRQKey, network.Nodes...), + } + + bootstrapVolumePath, err := antithesis.GetBootstrapVolumePath(targetPath) + if err != nil { + log.Fatalf("failed to get bootstrap volume path: %v", err) + } + + if err := antithesis.InitBootstrapDB(network, avalancheGoPath, pluginDir, bootstrapVolumePath); err != nil { + log.Fatalf("failed to initialize db volumes: %v", err) + } + + if err := antithesis.GenerateComposeConfig(network, nodeImageName, workloadImageName, targetPath); err != nil { + log.Fatalf("failed to generate config for docker-compose: %v", err) + } +} diff --git a/tests/antithesis/xsvm/main.go b/tests/antithesis/xsvm/main.go new file mode 100644 index 00000000000..84371752d4e --- /dev/null +++ b/tests/antithesis/xsvm/main.go @@ -0,0 +1,168 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "crypto/rand" + "log" + "math/big" + "os" + "time" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/antithesis" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/example/xsvm/api" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/status" + "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/transfer" +) + +const NumKeys = 5 + +func main() { + c, err := antithesis.NewConfig(os.Args) + if err != nil { + log.Fatalf("invalid config: %s", err) + } + + ctx := context.Background() + if err := antithesis.AwaitHealthyNodes(ctx, c.URIs); err != nil { + log.Fatalf("failed to await healthy nodes: %s", err) + } + + if len(c.ChainIDs) != 1 { + log.Fatalf("expected 1 chainID, saw %d", len(c.ChainIDs)) + } + chainID, err := ids.FromString(c.ChainIDs[0]) + if err != nil { + log.Fatalf("failed to parse chainID: %s", err) + } + + genesisWorkload := &workload{ + id: 0, + chainID: chainID, + key: genesis.VMRQKey, + addrs: set.Of(genesis.VMRQKey.Address()), + uris: c.URIs, + } + + workloads := make([]*workload, NumKeys) + workloads[0] = genesisWorkload + + initialAmount := 100 * units.KiloAvax + for i := 1; i < NumKeys; i++ { + key, err := secp256k1.NewPrivateKey() + if err != nil { + log.Fatalf("failed to generate key: %s", err) + } + + var ( + addr = key.Address() + baseStartTime = time.Now() + ) + transferTxStatus, err := transfer.Transfer( + ctx, + &transfer.Config{ + URI: c.URIs[0], + ChainID: chainID, + AssetID: chainID, + Amount: initialAmount, + To: addr, + PrivateKey: genesisWorkload.key, + }, + ) + if err != nil { + log.Fatalf("failed to issue initial funding transfer: %s", err) + } + log.Printf("issued initial funding transfer %s in %s", transferTxStatus.TxID, time.Since(baseStartTime)) + + genesisWorkload.confirmTransferTx(ctx, transferTxStatus) + + workloads[i] = &workload{ + id: i, + chainID: chainID, + key: key, + addrs: set.Of(addr), + uris: c.URIs, + } + } + + for _, w := range workloads[1:] { + go w.run(ctx) + } + genesisWorkload.run(ctx) +} + +type workload struct { + id int + chainID ids.ID + key *secp256k1.PrivateKey + addrs set.Set[ids.ShortID] + uris []string +} + +func (w *workload) run(ctx context.Context) { + timer := time.NewTimer(0) + if !timer.Stop() { + <-timer.C + } + + uri := w.uris[w.id%len(w.uris)] + + client := api.NewClient(uri, w.chainID.String()) + balance, err := client.Balance(ctx, w.key.Address(), w.chainID) + if err != nil { + log.Fatalf("failed to fetch balance: %s", err) + } + log.Printf("worker %d starting with a balance of %d", w.id, balance) + + for { + log.Printf("worker %d executing transfer", w.id) + destAddress, _ := w.addrs.Peek() + txStatus, err := transfer.Transfer( + ctx, + &transfer.Config{ + URI: uri, + ChainID: w.chainID, + AssetID: w.chainID, + Amount: units.Schmeckle, + To: destAddress, + PrivateKey: w.key, + }, + ) + if err != nil { + log.Printf("worker %d failed to issue transfer: %s", w.id, err) + } else { + log.Printf("worker %d issued transfer %s in %s", w.id, txStatus.TxID, time.Since(txStatus.StartTime)) + w.confirmTransferTx(ctx, txStatus) + } + + val, err := rand.Int(rand.Reader, big.NewInt(int64(time.Second))) + if err != nil { + log.Fatalf("failed to read randomness: %s", err) + } + + timer.Reset(time.Duration(val.Int64())) + select { + case <-ctx.Done(): + return + case <-timer.C: + } + } +} + +func (w *workload) confirmTransferTx(ctx context.Context, tx *status.TxIssuance) { + for _, uri := range w.uris { + client := api.NewClient(uri, w.chainID.String()) + if err := api.WaitForAcceptance(ctx, client, w.key.Address(), tx.Nonce); err != nil { + log.Printf("worker %d failed to confirm transaction %s on %s: %s", w.id, tx.TxID, uri, err) + return + } + } + log.Printf("worker %d confirmed transaction %s on all nodes", w.id, tx.TxID) +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 73c29b3bc83..9d235bc363d 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -38,7 +38,7 @@ var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { // Run only once in the first ginkgo process nodes := tmpnet.NewNodesOrPanic(flagVars.NodeCount()) - subnets := vms.XSVMSubnets(nodes...) + subnets := vms.XSVMSubnetsOrPanic(nodes...) return e2e.NewTestEnvironment( flagVars, &tmpnet.Network{ diff --git a/tests/e2e/vms/xsvm.go b/tests/e2e/vms/xsvm.go index 6e8d79c51d1..ebb00f882b1 100644 --- a/tests/e2e/vms/xsvm.go +++ b/tests/e2e/vms/xsvm.go @@ -5,22 +5,19 @@ package vms import ( "fmt" - "math" - "time" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/tests" "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/subnet" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/example/xsvm" "github.com/ava-labs/avalanchego/vms/example/xsvm/api" "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/export" "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/importtx" "github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue/transfer" - "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" ginkgo "github.com/onsi/ginkgo/v2" ) @@ -30,10 +27,14 @@ var ( subnetBName = "xsvm-b" ) -func XSVMSubnets(nodes ...*tmpnet.Node) []*tmpnet.Subnet { +func XSVMSubnetsOrPanic(nodes ...*tmpnet.Node) []*tmpnet.Subnet { + key, err := secp256k1.NewPrivateKey() + if err != nil { + panic(err) + } return []*tmpnet.Subnet{ - newXSVMSubnet(subnetAName, nodes...), - newXSVMSubnet(subnetBName, nodes...), + subnet.NewXSVMOrPanic(subnetAName, key, nodes...), + subnet.NewXSVMOrPanic(subnetBName, key, nodes...), } } @@ -141,39 +142,3 @@ var _ = ginkgo.Describe("[XSVM]", func() { require.Equal(units.Schmeckle, destinationBalance) }) }) - -func newXSVMSubnet(name string, nodes ...*tmpnet.Node) *tmpnet.Subnet { - if len(nodes) == 0 { - panic("a subnet must be validated by at least one node") - } - - key, err := secp256k1.NewPrivateKey() - if err != nil { - panic(err) - } - - genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, &genesis.Genesis{ - Timestamp: time.Now().Unix(), - Allocations: []genesis.Allocation{ - { - Address: key.Address(), - Balance: math.MaxUint64, - }, - }, - }) - if err != nil { - panic(err) - } - - return &tmpnet.Subnet{ - Name: name, - Chains: []*tmpnet.Chain{ - { - VMID: xsvm.ID, - Genesis: genesisBytes, - PreFundedKey: key, - }, - }, - ValidatorIDs: tmpnet.NodesToIDs(nodes...), - } -} diff --git a/tests/fixture/subnet/xsvm.go b/tests/fixture/subnet/xsvm.go new file mode 100644 index 00000000000..28fb017da5a --- /dev/null +++ b/tests/fixture/subnet/xsvm.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package subnet + +import ( + "math" + "time" + + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/example/xsvm" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" +) + +func NewXSVMOrPanic(name string, key *secp256k1.PrivateKey, nodes ...*tmpnet.Node) *tmpnet.Subnet { + if len(nodes) == 0 { + panic("a subnet must be validated by at least one node") + } + + genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, &genesis.Genesis{ + Timestamp: time.Now().Unix(), + Allocations: []genesis.Allocation{ + { + Address: key.Address(), + Balance: math.MaxUint64, + }, + }, + }) + if err != nil { + panic(err) + } + + return &tmpnet.Subnet{ + Name: name, + Chains: []*tmpnet.Chain{ + { + VMID: xsvm.ID, + Genesis: genesisBytes, + PreFundedKey: key, + }, + }, + ValidatorIDs: tmpnet.NodesToIDs(nodes...), + } +} diff --git a/tests/fixture/tmpnet/defaults.go b/tests/fixture/tmpnet/defaults.go index e2f0a2afd61..c5dbfeeebc9 100644 --- a/tests/fixture/tmpnet/defaults.go +++ b/tests/fixture/tmpnet/defaults.go @@ -35,25 +35,35 @@ const ( defaultConfigFilename = "config.json" ) -// A set of flags appropriate for testing. -func DefaultFlags() FlagsMap { - // Supply only non-default configuration to ensure that default values will be used. +// Flags appropriate for networks used for all types of testing. +func DefaultTestFlags() FlagsMap { return FlagsMap{ config.NetworkPeerListPullGossipFreqKey: "250ms", config.NetworkMaxReconnectDelayKey: "1s", - config.PublicIPKey: "127.0.0.1", - config.HTTPHostKey: "127.0.0.1", - config.StakingHostKey: "127.0.0.1", config.HealthCheckFreqKey: "2s", config.AdminAPIEnabledKey: true, config.IndexEnabledKey: true, - config.LogDisplayLevelKey: logging.Off.String(), // Display logging not needed since nodes run headless - config.LogLevelKey: logging.Debug.String(), - config.MinStakeDurationKey: DefaultMinStakeDuration.String(), - config.ProposerVMUseCurrentHeightKey: true, } } +// Flags appropriate for tmpnet networks. +func DefaultTmpnetFlags() FlagsMap { + // Supply only non-default configuration to ensure that default values will be used. + flags := FlagsMap{ + // Specific to tmpnet deployment + config.PublicIPKey: "127.0.0.1", + config.HTTPHostKey: "127.0.0.1", + config.StakingHostKey: "127.0.0.1", + config.LogDisplayLevelKey: logging.Off.String(), // Display logging not needed since nodes run headless + config.LogLevelKey: logging.Debug.String(), + // Specific to e2e testing + config.MinStakeDurationKey: DefaultMinStakeDuration.String(), + config.ProposerVMUseCurrentHeightKey: true, + } + flags.SetDefaults(DefaultTestFlags()) + return flags +} + // A set of chain configurations appropriate for testing. func DefaultChainConfigs() map[string]FlagsMap { return map[string]FlagsMap{ diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index bd5b1b914ef..e3efdd88cf5 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -198,7 +198,7 @@ func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, plugi if n.DefaultFlags == nil { n.DefaultFlags = FlagsMap{} } - n.DefaultFlags.SetDefaults(DefaultFlags()) + n.DefaultFlags.SetDefaults(DefaultTmpnetFlags()) if len(n.Nodes) == 1 { // Sybil protection needs to be disabled for a single node network to start @@ -214,8 +214,8 @@ func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, plugi } } - // Ensure pre-funded keys - if len(n.PreFundedKeys) == 0 { + // Ensure pre-funded keys if the genesis is not predefined + if n.Genesis == nil && len(n.PreFundedKeys) == 0 { keys, err := NewPrivateKeys(DefaultPreFundedKeyCount) if err != nil { return err @@ -294,7 +294,7 @@ func (n *Network) Create(rootDir string) error { } } - if n.Genesis == nil { + if n.NetworkID == 0 && n.Genesis == nil { // Pre-fund known legacy keys to support ad-hoc testing. Usage of a legacy key will // require knowing the key beforehand rather than retrieving it from the set of pre-funded // keys exposed by a network. Since allocation will not be exclusive, a test using a @@ -525,10 +525,13 @@ func (n *Network) EnsureNodeConfig(node *Node) error { // Set fields including the network path if len(n.Dir) > 0 { defaultFlags := FlagsMap{ - config.GenesisFileKey: n.getGenesisPath(), config.ChainConfigDirKey: n.getChainConfigDir(), } + if n.Genesis != nil { + defaultFlags[config.GenesisFileKey] = n.getGenesisPath() + } + // Only set the subnet dir if it exists or the node won't start. subnetDir := n.getSubnetDir() if _, err := os.Stat(subnetDir); err == nil { @@ -540,7 +543,7 @@ func (n *Network) EnsureNodeConfig(node *Node) error { node.Flags.SetDefaults(defaultFlags) // Ensure the node's data dir is configured - dataDir := node.getDataDir() + dataDir := node.GetDataDir() if len(dataDir) == 0 { // NodeID will have been set by EnsureKeys dataDir = filepath.Join(n.Dir, node.NodeID.String()) @@ -637,9 +640,6 @@ func (n *Network) CreateSubnets(ctx context.Context, w io.Writer) error { return nil } - // Ensure the in-memory subnet state - n.Subnets = append(n.Subnets, createdSubnets...) - // Ensure the pre-funded key changes are persisted to disk if err := n.Write(); err != nil { return err diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go index 99777e674c0..e29cb92a296 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -190,7 +190,7 @@ func (n *Node) readState() error { return n.getRuntime().readState() } -func (n *Node) getDataDir() string { +func (n *Node) GetDataDir() string { return cast.ToString(n.Flags[config.DataDirKey]) } diff --git a/tests/fixture/tmpnet/node_config.go b/tests/fixture/tmpnet/node_config.go index 1f47c926dfc..4752b2c343c 100644 --- a/tests/fixture/tmpnet/node_config.go +++ b/tests/fixture/tmpnet/node_config.go @@ -18,7 +18,7 @@ import ( // (reading/writing configuration) and node.go (orchestration). func (n *Node) getFlagsPath() string { - return filepath.Join(n.getDataDir(), "flags.json") + return filepath.Join(n.GetDataDir(), "flags.json") } func (n *Node) readFlags() error { @@ -46,7 +46,7 @@ func (n *Node) writeFlags() error { } func (n *Node) getConfigPath() string { - return filepath.Join(n.getDataDir(), defaultConfigFilename) + return filepath.Join(n.GetDataDir(), defaultConfigFilename) } func (n *Node) readConfig() error { @@ -95,7 +95,7 @@ func (n *Node) Read() error { } func (n *Node) Write() error { - if err := os.MkdirAll(n.getDataDir(), perms.ReadWriteExecute); err != nil { + if err := os.MkdirAll(n.GetDataDir(), perms.ReadWriteExecute); err != nil { return fmt.Errorf("failed to create node dir: %w", err) } @@ -106,7 +106,7 @@ func (n *Node) Write() error { } func (n *Node) writeMetricsSnapshot(data []byte) error { - metricsDir := filepath.Join(n.getDataDir(), "metrics") + metricsDir := filepath.Join(n.GetDataDir(), "metrics") if err := os.MkdirAll(metricsDir, perms.ReadWriteExecute); err != nil { return fmt.Errorf("failed to create metrics dir: %w", err) } diff --git a/tests/fixture/tmpnet/node_process.go b/tests/fixture/tmpnet/node_process.go index f2a9c7ff628..a866cec63db 100644 --- a/tests/fixture/tmpnet/node_process.go +++ b/tests/fixture/tmpnet/node_process.go @@ -122,7 +122,7 @@ func (p *NodeProcess) Start(w io.Writer) error { } // Determine appropriate level of node description detail - dataDir := p.node.getDataDir() + dataDir := p.node.GetDataDir() nodeDescription := fmt.Sprintf("node %q", p.node.NodeID) if p.node.IsEphemeral { nodeDescription = "ephemeral " + nodeDescription @@ -201,7 +201,7 @@ func (p *NodeProcess) IsHealthy(ctx context.Context) (bool, error) { } func (p *NodeProcess) getProcessContextPath() string { - return filepath.Join(p.node.getDataDir(), config.DefaultProcessContextFilename) + return filepath.Join(p.node.GetDataDir(), config.DefaultProcessContextFilename) } func (p *NodeProcess) waitForProcessContext(ctx context.Context) error { @@ -294,7 +294,7 @@ func (p *NodeProcess) writeMonitoringConfig() error { } promtailLabels := FlagsMap{ - "__path__": filepath.Join(p.node.getDataDir(), "logs", "*.log"), + "__path__": filepath.Join(p.node.GetDataDir(), "logs", "*.log"), } promtailLabels.SetDefaults(commonLabels) promtailConfig := []FlagsMap{ diff --git a/vms/example/xsvm/Dockerfile b/vms/example/xsvm/Dockerfile new file mode 100644 index 00000000000..8e7c4c5bba9 --- /dev/null +++ b/vms/example/xsvm/Dockerfile @@ -0,0 +1,31 @@ +# The version is supplied as a build argument rather than hard-coded +# to minimize the cost of version changes. +ARG GO_VERSION + +# AVALANCHEGO_NODE_IMAGE needs to identify an existing node image and should include the tag +ARG AVALANCHEGO_NODE_IMAGE + +# ============= Compilation Stage ================ +FROM golang:$GO_VERSION-bullseye AS builder + +WORKDIR /build + +# Copy and download avalanche dependencies using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the code into the container +COPY . . + +# Build xsvm +RUN ./scripts/build_xsvm.sh + +# ============= Cleanup Stage ================ +FROM $AVALANCHEGO_NODE_IMAGE AS execution + +# Copy the xsvm binary to the default plugin path +RUN mkdir -p /root/.avalanchego/plugins +COPY --from=builder /build/build/xsvm /root/.avalanchego/plugins/v3m4wPxaHpvGr8qfMeyK6PRW3idZrPHmYcMTt7oXdK47yurVH + +# The node image's entrypoint will be reused.