Skip to content

Commit 4159a59

Browse files
[antithesis] Add test setup for xsvm (#2982)
Signed-off-by: marun <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent 6a894d0 commit 4159a59

27 files changed

+806
-178
lines changed

.github/workflows/ci.yml

+12-2
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ jobs:
265265
- name: Check image build
266266
shell: bash
267267
run: bash -x scripts/tests.build_image.sh
268-
test_build_antithesis_avalanchego_image:
269-
name: Antithesis avalanchego build
268+
test_build_antithesis_avalanchego_images:
269+
name: Build Antithesis avalanchego images
270270
runs-on: ubuntu-latest
271271
steps:
272272
- uses: actions/checkout@v4
@@ -275,6 +275,16 @@ jobs:
275275
run: bash -x scripts/tests.build_antithesis_images.sh
276276
env:
277277
TEST_SETUP: avalanchego
278+
test_build_antithesis_xsvm_images:
279+
name: Build Antithesis xsvm images
280+
runs-on: ubuntu-latest
281+
steps:
282+
- uses: actions/checkout@v4
283+
- name: Check image build for xsvm test setup
284+
shell: bash
285+
run: bash -x scripts/tests.build_antithesis_images.sh
286+
env:
287+
TEST_SETUP: xsvm
278288
govulncheck:
279289
runs-on: ubuntu-latest
280290
name: govulncheck

.github/workflows/publish_antithesis_images.yml

+7
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,10 @@ jobs:
3131
IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
3232
TAG: latest
3333
TEST_SETUP: avalanchego
34+
35+
- name: Build and push images for xsvm test setup
36+
run: bash -x ./scripts/build_antithesis_images.sh
37+
env:
38+
IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
39+
TAG: latest
40+
TEST_SETUP: xsvm

scripts/build_antithesis_images.sh

+59-12
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ set -euo pipefail
55
# Builds docker images for antithesis testing.
66

77
# e.g.,
8-
# ./scripts/build_antithesis_images.sh # Build local images
9-
# IMAGE_PREFIX=<registry>/<repo> TAG=latest ./scripts/build_antithesis_images.sh # Specify a prefix to enable image push and use a specific tag
8+
# TEST_SETUP=avalanchego ./scripts/build_antithesis_images.sh # Build local images for avalanchego
9+
# TEST_SETUP=avalanchego NODE_ONLY=1 ./scripts/build_antithesis_images.sh # Build only a local node image for avalanchego
10+
# TEST_SETUP=xsvm ./scripts/build_antithesis_images.sh # Build local images for xsvm
11+
# TEST_SETUP=xsvm IMAGE_PREFIX=<registry>/<repo> TAG=latest ./scripts/build_antithesis_images.sh # Specify a prefix to enable image push and use a specific tag
1012

1113
# Directory above this script
1214
AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd )
@@ -28,11 +30,13 @@ GO_VERSION="$(go list -m -f '{{.GoVersion}}')"
2830
function build_images {
2931
local test_setup=$1
3032
local uninstrumented_node_dockerfile=$2
33+
local image_prefix=$3
34+
local node_only=${4:-}
3135

3236
# Define image names
3337
local base_image_name="antithesis-${test_setup}"
34-
if [[ -n "${IMAGE_PREFIX}" ]]; then
35-
base_image_name="${IMAGE_PREFIX}/${base_image_name}"
38+
if [[ -n "${image_prefix}" ]]; then
39+
base_image_name="${image_prefix}/${base_image_name}"
3640
fi
3741
local node_image_name="${base_image_name}-node:${TAG}"
3842
local workload_image_name="${base_image_name}-workload:${TAG}"
@@ -49,22 +53,65 @@ function build_images {
4953
fi
5054

5155
# Define default build command
52-
local docker_cmd="docker buildx build --build-arg GO_VERSION=${GO_VERSION}"
53-
if [[ -n "${IMAGE_PREFIX}" ]]; then
56+
local docker_cmd="docker buildx build --build-arg GO_VERSION=${GO_VERSION} --build-arg NODE_IMAGE=${node_image_name}"
57+
58+
if [[ "${test_setup}" == "xsvm" ]]; then
59+
# The xsvm node image is built on the avalanchego node image, which is assumed to have already been
60+
# built. The image name doesn't include the image prefix because it is not intended to be pushed.
61+
docker_cmd="${docker_cmd} --build-arg AVALANCHEGO_NODE_IMAGE=antithesis-avalanchego-node:${TAG}"
62+
fi
63+
64+
# Build node image first to allow the workload image to use it.
65+
${docker_cmd} -t "${node_image_name}" -f "${node_dockerfile}" "${AVALANCHE_PATH}"
66+
if [[ -n "${image_prefix}" ]]; then
5467
# Push images with an image prefix since the prefix defines a registry location
5568
docker_cmd="${docker_cmd} --push"
5669
fi
5770

58-
# Build node image first to allow the config and workload image builds to use it.
59-
${docker_cmd} -t "${node_image_name}" -f "${node_dockerfile}" "${AVALANCHE_PATH}"
60-
${docker_cmd} --build-arg NODE_IMAGE="${node_image_name}" -t "${workload_image_name}" -f "${base_dockerfile}.workload" "${AVALANCHE_PATH}"
61-
${docker_cmd} --build-arg IMAGE_TAG="${TAG}" -t "${config_image_name}" -f "${base_dockerfile}.config" "${AVALANCHE_PATH}"
71+
if [[ -n "${node_only}" ]]; then
72+
# Skip building the config and workload images. Supports building the avalanchego
73+
# node image as the base image for the xsvm node image.
74+
return
75+
fi
76+
77+
TARGET_PATH="${AVALANCHE_PATH}/build/antithesis/${test_setup}"
78+
if [[ -d "${TARGET_PATH}" ]]; then
79+
# Ensure the target path is empty before generating the compose config
80+
rm -r "${TARGET_PATH:?}"
81+
fi
82+
83+
# Define the env vars for the compose config generation
84+
COMPOSE_ENV="TARGET_PATH=${TARGET_PATH} IMAGE_TAG=${TAG}"
85+
86+
if [[ "${test_setup}" == "xsvm" ]]; then
87+
# Ensure avalanchego and xsvm binaries are available to create an initial db state that includes subnets.
88+
"${AVALANCHE_PATH}"/scripts/build.sh
89+
"${AVALANCHE_PATH}"/scripts/build_xsvm.sh
90+
COMPOSE_ENV="${COMPOSE_ENV} AVALANCHEGO_PATH=${AVALANCHE_PATH}/build/avalanchego AVALANCHEGO_PLUGIN_DIR=${HOME}/.avalanchego/plugins"
91+
fi
92+
93+
# Generate compose config for copying into the config image
94+
# shellcheck disable=SC2086
95+
env ${COMPOSE_ENV} go run "${AVALANCHE_PATH}/tests/antithesis/${test_setup}/gencomposeconfig"
96+
97+
# Build the config image
98+
${docker_cmd} -t "${config_image_name}" -f "${base_dockerfile}.config" "${AVALANCHE_PATH}"
99+
100+
# Build the workload image
101+
${docker_cmd} -t "${workload_image_name}" -f "${base_dockerfile}.workload" "${AVALANCHE_PATH}"
62102
}
63103

64104
TEST_SETUP="${TEST_SETUP:-}"
65105
if [[ "${TEST_SETUP}" == "avalanchego" ]]; then
66-
build_images avalanchego "${AVALANCHE_PATH}/Dockerfile"
106+
build_images avalanchego "${AVALANCHE_PATH}/Dockerfile" "${IMAGE_PREFIX}" "${NODE_ONLY:-}"
107+
elif [[ "${TEST_SETUP}" == "xsvm" ]]; then
108+
# Only build the node image to use as the base for the xsvm image. Provide an empty
109+
# image prefix (the 3rd argument) to prevent the image from being pushed
110+
NODE_ONLY=1
111+
build_images avalanchego "${AVALANCHE_PATH}/Dockerfile" "" "${NODE_ONLY}"
112+
113+
build_images xsvm "${AVALANCHE_PATH}/vms/example/xsvm/Dockerfile" "${IMAGE_PREFIX}"
67114
else
68-
echo "TEST_SETUP must be set. Valid values are 'avalanchego'"
115+
echo "TEST_SETUP must be set. Valid values are 'avalanchego' or 'xsvm'"
69116
exit 255
70117
fi
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Directory above this script
6+
AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd )
7+
# Load the constants
8+
source "$AVALANCHE_PATH"/scripts/constants.sh
9+
10+
echo "Building Workload..."
11+
go build -o "$AVALANCHE_PATH/build/antithesis-xsvm-workload" "$AVALANCHE_PATH/tests/antithesis/xsvm/"*.go

scripts/tests.build_antithesis_images.sh

+13-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ set -euo pipefail
1010
# 4. Stopping the workload and its target network
1111
#
1212

13+
# e.g.,
14+
# TEST_SETUP=avalanchego ./scripts/tests.build_antithesis_images.sh # Test build of images for avalanchego test setup
15+
# DEBUG=1 TEST_SETUP=avalanchego ./scripts/tests.build_antithesis_images.sh # Retain the temporary compose path for troubleshooting
16+
1317
AVALANCHE_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd .. && pwd )
1418

1519
# 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
2731

2832
# Create a temporary directory to write the compose configuration to
2933
TMPDIR="$(mktemp -d)"
34+
echo "using temporary directory ${TMPDIR} as the docker-compose path"
35+
3036
COMPOSE_FILE="${TMPDIR}/docker-compose.yml"
3137
COMPOSE_CMD="docker-compose -f ${COMPOSE_FILE}"
3238

@@ -36,8 +42,10 @@ function cleanup {
3642
docker rm "${CONTAINER_NAME}"
3743
echo "stopping and removing the docker compose project"
3844
${COMPOSE_CMD} down --volumes
39-
echo "removing temporary dir"
40-
rm -rf "${TMPDIR}"
45+
if [[ -z "${DEBUG:-}" ]]; then
46+
echo "removing temporary dir"
47+
rm -rf "${TMPDIR}"
48+
fi
4149
}
4250
trap cleanup EXIT
4351

@@ -47,9 +55,10 @@ docker cp "${CONTAINER_NAME}":/docker-compose.yml "${COMPOSE_FILE}"
4755
# Copy the volume paths out of the container
4856
docker cp "${CONTAINER_NAME}":/volumes "${TMPDIR}/"
4957

50-
# Run the docker compose project for one minute without error
58+
# Run the docker compose project for 30 seconds without error. Local
59+
# network bootstrap is ~6s, but github workers can be much slower.
5160
${COMPOSE_CMD} up -d
52-
sleep 60
61+
sleep 30
5362
if ${COMPOSE_CMD} ps -q | xargs docker inspect -f '{{ .State.Status }}' | grep -v 'running'; then
5463
echo "An error occurred."
5564
exit 255

tests/antithesis/README.md

+60-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ enables discovery and reproduction of anomalous behavior.
88

99
## Package details
1010

11-
| Filename | Purpose |
12-
|:-------------|:----------------------------------------------------------------------------------|
13-
| compose.go | Enables generation of Docker Compose project files for antithesis testing. |
14-
| avalanchego/ | Contains resources supporting antithesis testing of avalanchego's primary chains. |
15-
11+
| Filename | Purpose |
12+
|:---------------|:-----------------------------------------------------------------------------------|
13+
| compose.go | Generates Docker Compose project file and initial database for antithesis testing. |
14+
| config.go | Defines common flags for the workload binary. |
15+
| init_db.go | Initializes initial db state for subnet testing. |
16+
| node_health.go | Helper to check node health. |
17+
| avalanchego/ | Defines an antithesis test setup for avalanchego's primary chains. |
18+
| xsvm/ | Defines an antithesis test setup for the xsvm VM. |
1619

1720
## Instrumentation
1821

@@ -45,3 +48,55 @@ a test setup:
4548
In addition, github workflows are suggested to ensure
4649
`scripts/tests.build_antithesis_images.sh` runs against PRs and
4750
`scripts/build_antithesis_images.sh` runs against pushes.
51+
52+
## Troubleshooting a test setup
53+
54+
### Running a workload directly
55+
56+
The workload of the 'avalanchego' test setup can be invoked against an
57+
arbitrary network:
58+
59+
```bash
60+
$ AVAWL_URIS="http://10.0.20.3:9650 http://10.0.20.4:9650" go run ./tests/antithesis/avalanchego
61+
```
62+
63+
The workload of a subnet test setup like 'xsvm' additionally requires
64+
a network with a configured chain for the xsvm VM and the ID for that
65+
chain needs to be provided to the workload:
66+
67+
```bash
68+
$ AVAWL_URIS=... CHAIN_IDS="2S9ypz...AzMj9" go run ./tests/antithesis/xsvm
69+
```
70+
71+
### Running a workload with docker-compose
72+
73+
Running the test script for a given test setup with the `DEBUG` flag
74+
set will avoid cleaning up the the temporary directory where the
75+
docker-compose setup is written to. This will allow manual invocation of
76+
docker-compose to see the log output of the workload.
77+
78+
```bash
79+
$ DEBUG=1 ./scripts/tests.build_antithesis_images.sh
80+
```
81+
82+
After the test script has terminated, the name of the temporary
83+
directory will appear in the output of the script:
84+
85+
```
86+
...
87+
using temporary directory /tmp/tmp.E6eHdDr4ln as the docker-compose path"
88+
...
89+
```
90+
91+
Running compose from the temporary directory will ensure the workload
92+
output appears on stdout for inspection:
93+
94+
```bash
95+
$ cd [temporary directory]
96+
97+
# Start the compose project
98+
$ docker-compose up
99+
100+
# Cleanup the compose project
101+
$ docker-compose down --volumes
102+
```
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,5 @@
1-
# The version is supplied as a build argument rather than hard-coded
2-
# to minimize the cost of version changes.
3-
ARG GO_VERSION
4-
5-
# ============= Compilation Stage ================
6-
FROM golang:$GO_VERSION-bullseye AS builder
7-
8-
WORKDIR /build
9-
# Copy and download avalanche dependencies using go mod
10-
COPY go.mod .
11-
COPY go.sum .
12-
RUN go mod download
13-
14-
# Copy the code into the container
15-
COPY . .
16-
17-
# IMAGE_TAG should be set to the tag for the images in the generated
18-
# docker compose file.
19-
ARG IMAGE_TAG=latest
20-
21-
# Generate docker compose configuration
22-
RUN TARGET_PATH=./build IMAGE_TAG="$IMAGE_TAG" go run ./tests/antithesis/avalanchego/gencomposeconfig
23-
24-
# ============= Cleanup Stage ================
251
FROM scratch AS execution
262

27-
# Copy the docker compose file and volumes into the container
28-
COPY --from=builder /build/build/docker-compose.yml /docker-compose.yml
29-
COPY --from=builder /build/build/volumes /volumes
3+
# Copy config artifacts from the build path. For simplicity, artifacts
4+
# are built outside of the docker image.
5+
COPY ./build/antithesis/avalanchego/ /

tests/antithesis/avalanchego/Dockerfile.node

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ RUN mkdir -p /symbols
5656
COPY --from=builder /avalanchego_instrumented/symbols /symbols
5757
COPY --from=builder /opt/antithesis/lib/libvoidstar.so /usr/lib/libvoidstar.so
5858

59+
# Use the same path as the uninstrumented node image for consistency
60+
WORKDIR /avalanchego/build
61+
5962
# Copy the executable into the container
6063
COPY --from=builder /avalanchego_instrumented/customer/build/avalanchego ./avalanchego
6164

tests/antithesis/avalanchego/main.go

+6-38
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
"os"
1212
"time"
1313

14-
"github.com/ava-labs/avalanchego/api/health"
1514
"github.com/ava-labs/avalanchego/database"
1615
"github.com/ava-labs/avalanchego/genesis"
1716
"github.com/ava-labs/avalanchego/ids"
1817
"github.com/ava-labs/avalanchego/snow/choices"
18+
"github.com/ava-labs/avalanchego/tests/antithesis"
1919
"github.com/ava-labs/avalanchego/utils/constants"
2020
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
2121
"github.com/ava-labs/avalanchego/utils/set"
@@ -38,13 +38,15 @@ import (
3838
const NumKeys = 5
3939

4040
func main() {
41-
c, err := NewConfig(os.Args)
41+
c, err := antithesis.NewConfig(os.Args)
4242
if err != nil {
4343
log.Fatalf("invalid config: %s", err)
4444
}
4545

4646
ctx := context.Background()
47-
awaitHealthyNodes(ctx, c.URIs)
47+
if err := antithesis.AwaitHealthyNodes(ctx, c.URIs); err != nil {
48+
log.Fatalf("failed to await healthy nodes: %s", err)
49+
}
4850

4951
kc := secp256k1fx.NewKeychain(genesis.EWOQKey)
5052
walletSyncStartTime := time.Now()
@@ -99,8 +101,7 @@ func main() {
99101
},
100102
}})
101103
if err != nil {
102-
log.Printf("failed to issue initial funding X-chain baseTx: %s", err)
103-
return
104+
log.Fatalf("failed to issue initial funding X-chain baseTx: %s", err)
104105
}
105106
log.Printf("issued initial funding X-chain baseTx %s in %s", baseTx.ID(), time.Since(baseStartTime))
106107

@@ -133,39 +134,6 @@ func main() {
133134
genesisWorkload.run(ctx)
134135
}
135136

136-
func awaitHealthyNodes(ctx context.Context, uris []string) {
137-
for _, uri := range uris {
138-
awaitHealthyNode(ctx, uri)
139-
}
140-
log.Println("all nodes reported healthy")
141-
}
142-
143-
func awaitHealthyNode(ctx context.Context, uri string) {
144-
client := health.NewClient(uri)
145-
ticker := time.NewTicker(100 * time.Millisecond)
146-
defer ticker.Stop()
147-
148-
log.Printf("awaiting node health at %s", uri)
149-
for {
150-
res, err := client.Health(ctx, nil)
151-
switch {
152-
case err != nil:
153-
log.Printf("node couldn't be reached at %s", uri)
154-
case res.Healthy:
155-
log.Printf("node reported healthy at %s", uri)
156-
return
157-
default:
158-
log.Printf("node reported unhealthy at %s", uri)
159-
}
160-
161-
select {
162-
case <-ticker.C:
163-
case <-ctx.Done():
164-
log.Printf("node health check cancelled at %s", uri)
165-
}
166-
}
167-
}
168-
169137
type workload struct {
170138
id int
171139
wallet primary.Wallet

0 commit comments

Comments
 (0)