Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node: Add Transfer Verifier mechanism #4169

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2756069
node: Add Transfer Verifier mechanism
johnsaigle Aug 28, 2024
9f71d89
address pr comments
pleasew8t Dec 9, 2024
3930e48
rename CLI arguments
johnsaigle Dec 9, 2024
d899cbc
rename eth to evm where appropriate
johnsaigle Dec 9, 2024
015cf10
add parameters for loki logging
johnsaigle Dec 9, 2024
a93ad7d
resolve errcheck problems
johnsaigle Dec 9, 2024
ccdf49f
Add receipt hash as a parameter for ProcessEvent
johnsaigle Dec 16, 2024
eb6aa6e
Refactor so wrappedNativeAddress is passed by CLI
johnsaigle Dec 16, 2024
ae28d47
Fix overwritten variable in TV EVM command
johnsaigle Dec 17, 2024
8316ee9
Add chain validation; add localnet chain id 1337 to sdk
johnsaigle Dec 17, 2024
95a1db9
Translate EVM chain IDs to wormhole chain IDs where appropriate
johnsaigle Dec 18, 2024
deba549
Hardcode anvil's chain ID as mainnet for testing; revert SDK changes
johnsaigle Dec 18, 2024
ec48ce5
remove TODO
pleasew8t Dec 19, 2024
694622b
add handling for ctx.Done()
johnsaigle Jan 14, 2025
812d458
Add readme for transfer verifier integration tests
johnsaigle Jan 15, 2025
7bf072c
Add package-level readme for transfer verifier
johnsaigle Jan 15, 2025
55e070e
Address issues related to EVM endianness
johnsaigle Jan 16, 2025
0585373
Cleanup tiltfile
johnsaigle Jan 20, 2025
2ce4fb5
Change from anvil user0 to user1. Reduce number of accounts that rece…
johnsaigle Jan 20, 2025
3dd3863
Change Dockerfile.cast to conform to ethereum/Dockerfile
johnsaigle Jan 20, 2025
08a9ac5
Remove unused API version key
johnsaigle Jan 20, 2025
339f20d
Rename files and pkg according to Go conventions
johnsaigle Jan 20, 2025
484f0f7
Update golangci exclusion to match the new filename
johnsaigle Jan 20, 2025
0271a4f
Remove old test/coverage files
johnsaigle Jan 20, 2025
e6c3e87
Remove unused test yaml file
johnsaigle Jan 21, 2025
6f35275
Remove extraneous evm label for transfer verifier tests
johnsaigle Jan 21, 2025
53b9b75
Change transfer verifier testing to use anvil's account[13] instead o…
johnsaigle Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ issues:
text: "^func.*supervisor.*(waitSettle|waitSettleError).*$"
linters:
- unused
# This file contains hard-coded Sui core contract addresses that are marked as hardcoded credentials.
- path: pkg/txverifier/sui_test.go

text: "G101: Potential hardcoded credentials"
34 changes: 34 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,11 @@ if evm2:
)


# Note that ci_tests requires other resources in order to build properly:
# - eth-devnet -- required by: accountant_tests, ntt_accountant_tests, tx-verifier
# - eth-devnet2 -- required by: accountant_tests, ntt_accountant_tests
# - wormchain -- required by: accountant_tests, ntt_accountant_tests
# - solana -- required by: spydk-ci-tests
if ci_tests:
docker_build(
ref = "sdk-test-image",
Expand Down Expand Up @@ -635,6 +640,16 @@ if ci_tests:
sync("./testing", "/app/testing"),
],
)
docker_build(
ref = "tx-verifier-monitor",
context = "./devnet/tx-verifier-monitor/",
dockerfile = "./devnet/tx-verifier-monitor/Dockerfile"
)
docker_build(
ref = "tx-verifier-test",
context = "./devnet/tx-verifier-monitor/",
dockerfile = "./devnet/tx-verifier-monitor/Dockerfile.cast"
)

k8s_yaml_with_ns(
encode_yaml_stream(
Expand All @@ -644,6 +659,11 @@ if ci_tests:
"BOOTSTRAP_PEERS", str(ccqBootstrapPeers)),
"MAX_WORKERS", max_workers))
)

# transfer-verifier -- daemon and log monitoring
k8s_yaml_with_ns("devnet/tx-verifier.yaml")

k8s_yaml_with_ns("devnet/tx-verifier-test.yaml")

# separate resources to parallelize docker builds
k8s_resource(
Expand Down Expand Up @@ -676,6 +696,20 @@ if ci_tests:
trigger_mode = trigger_mode,
resource_deps = [], # testing/querysdk.sh handles waiting for query-server, not having deps gets the build earlier
)
# launches tx-verifier binary and sets up monitoring script
k8s_resource(
"tx-verifier-with-monitor",
resource_deps = ["eth-devnet"],
labels = ["tx-verifier"],
trigger_mode = trigger_mode,
)
# triggers the integration tests that will be detected by the monitor
k8s_resource(
"tx-verifier-test",
resource_deps = ["eth-devnet", "tx-verifier-with-monitor"],
labels = ["tx-verifier"],
trigger_mode = trigger_mode,
)

if terra_classic:
docker_build(
Expand Down
5 changes: 4 additions & 1 deletion devnet/eth-devnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ spec:
containers:
- name: anvil
image: eth-node
# This command generates additional accounts compared to the default of 10. The purpose is to use dedicated
# accounts for different aspects of the test suite. When adding new integration tests, consider increasing
# the number of accounts below and using a fresh key for the new tests.
command:
- anvil
- --silent
- --mnemonic=myth like bonus scare over problem client lizard pioneer submit female collect
- --block-time=1
- --host=0.0.0.0
- --accounts=13
- --accounts=14
- --chain-id=1337
ports:
- containerPort: 8545
Expand Down
5 changes: 4 additions & 1 deletion devnet/eth-devnet2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ spec:
containers:
- name: anvil
image: eth-node
# This command generates additional accounts compared to the default of 10. The purpose is to use dedicated
# accounts for different aspects of the test suite. When adding new integration tests, consider increasing
# the number of accounts below and using a fresh key for the new tests.
command:
- anvil
- --silent
- --mnemonic=myth like bonus scare over problem client lizard pioneer submit female collect
- --block-time=1
- --host=0.0.0.0
- --accounts=13
- --accounts=14
- --chain-id=1397
ports:
- containerPort: 8545
Expand Down
10 changes: 10 additions & 0 deletions devnet/tx-verifier-monitor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# There's nothing special about this version, it is simply the `latest` as of
# the creation date of this file.
FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a

RUN apk add --no-cache inotify-tools

COPY monitor.sh /monitor.sh
RUN chmod +x /monitor.sh

CMD ["/monitor.sh"]
13 changes: 13 additions & 0 deletions devnet/tx-verifier-monitor/Dockerfile.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These versions are pinned to match the Dockerfile in the `ethereum/`
# directory. Otherwise, there is nothing special about them and they can be
# updated alongside the other Dockerfile.
FROM --platform=linux/amd64 ghcr.io/foundry-rs/foundry:nightly-55bf41564f605cae3ca4c95ac5d468b1f14447f9@sha256:8c15d322da81a6deaf827222e173f3f81c653136a3518d5eeb41250a0f2e17ea as foundry
# node is required to install Foundry
FROM node:19.6.1-slim@sha256:a1ba21bf0c92931d02a8416f0a54daad66cb36a85d2b73af9d73b044f5f57cfc

COPY --from=foundry /usr/local/bin/cast /bin/cast

COPY transfer-verifier-test.sh /transfer-verifier-test.sh
RUN chmod +x /transfer-verifier-test.sh

CMD ["/transfer-verifier-test.sh"]
64 changes: 64 additions & 0 deletions devnet/tx-verifier-monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Transfer Verifier -- Integration Tests

## EVM Integration Tests

### Overview

The Transfer Verifier tests involve interacting with the local ethereum devnet defined by the Tilt set-up in this repository.

The basic idea is as follows:
* Interact with the local Ethereum testnet. This should already have important pieces such as the Token Bridge and Core Bridge deployed.
* Use `cast` from the foundry tool set to simulate malicious interactions with the Token Bridge.
* Transfer Verifier detects the malicious messages and emits errors about what went wrong.
* The error messages are logged to a file
* A "monitor" script is used to detect the expected error message, waiting until the file is written to
* If the monitor script sees the expected error message in the error log, it terminates

## Components

### Scripts

#### transfer-verifier-test.sh

Contains the `cast` commands that simulate malicious interactions with the Token Bridge and Core Bridge. It is able to broadcast
transactions to the `anvil` instance that powers the Ethereum testnet while being able to impersonate arbitrary senders.

This lets us perform actions that otherwise should be impossible, like causing a Publish Message event to be emitted from the Core Bridge
without a corresponding deposit or transfer into the Token Bridge.

#### monitor.sh

A bash script that monitors the error log file for a specific error pattern. It runs in an infinite loop so it will
not exit until the error pattern is detected.

The error pattern is defined in `wormhole/devnet/tx-verifier.yaml` and matches an error string in the Transfer Verifier package.

Once the pattern is detected, a success message is logged to a status file. Currently this is unused but this set-up
could be modified to detect that this script has written the success message to figure out whether the whole test completed successfully.

### Pods

The files detailed below each have a primary role and are responsible for running one of the main pieces of the test functionality:

* The Transfer Verifier binary which monitors the state of the local Ethereum network
* The integration test script that generates activity that the Transfer Verifier classifies as malicious
* The monitor script which ensures that the Transfer Verifier successfully
detected the error we expected, and signals to Tilt that the overall test has
succeeded

#### devnet/tx-verifier.yaml

Runs the Transfer Verifier binary and redirects its STDERR to the error log file. This allows the output of the binary
to be monitored by `monitor.sh`.

#### devnet/tx-verifier-test.yaml

Runs the `transfer-verifier-test.sh` script which simulates malicious Token Bridge activity. Defines the RPC URL used
by that bash script, which corresponds to the `anvil` instance created in the Ethereum devnet.

#### devnet/tx-verifier-monitor.yaml

Defines the expected error string that should be emitted by the Transfer Verifier code assuming that it successfully recognizes
the malicious Token Bridge activity simulated by the `cast` commands in `transfer-verifier-test.sh`.

It also defines a path to the log file that contains this string.
25 changes: 25 additions & 0 deletions devnet/tx-verifier-monitor/monitor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh

log_file="${ERROR_LOG_PATH:-/logs/error.log}"
error_pattern="${ERROR_PATTERN:-ERROR}"
status_file="/logs/status"

# Wait for log file to exist and be non-empty
while [ ! -s "${log_file}" ]; do
echo "Waiting for ${log_file} to be created and contain data..."
sleep 5
done

# Initialize status
echo "RUNNING" > "$status_file"
echo "Monitoring file '${log_file}' for error pattern: '${error_pattern}'"

# Watch for changes in the log file. If we find the error pattern that means we have
# succeeded. (Transfer verifier should correctly detect errors.
inotifywait -m -e modify "${log_file}" | while read -r directory events filename; do
if grep -q "$error_pattern" "$log_file"; then
echo "SUCCESS" > "$status_file"
echo "Found error pattern. Exiting."
exit 0
fi
done
120 changes: 120 additions & 0 deletions devnet/tx-verifier-monitor/transfer-verifier-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -euo pipefail

RPC="${RPC_URL:-ws://eth-devnet:8545}"

# mainnet values
# export CORE_CONTRACT="0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"
# export TOKEN_BRIDGE_CONTRACT="0x3ee18B2214AFF97000D974cf647E7C347E8fa585"

# TODO these could be CLI params from the sh/devnet script
CORE_BRIDGE_CONTRACT=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550
TOKEN_BRIDGE_CONTRACT=0x0290FB167208Af455bB137780163b7B7a9a10C16

MNEMONIC=0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d

ERC20_ADDR="0x47bdB2D7d6528C760b6f228b3B8F9F650169a10f" # Test token A

VALUE="1000" # Wei value sent as msg.value
TRANSFER_AMOUNT="10"

# This account is generated by anvil and can be confirmed by running `anvil --accounts=13`.
# The accounts at other indices are used by other tests in the test suite, so
# account[13] is used here to help encapsulate the tests.
ANVIL_USER="0x64E078A8Aa15A41B85890265648e965De686bAE6"
ETH_WHALE="${ANVIL_USER}"
FROM="${ETH_WHALE}"
# Anvil user1 normalized to Wormhole size. (The value itself it unchecked but must have this format.)
RECIPIENT="0x00000000000000000000000064E078A8Aa15A41B85890265648e965De686bAE6"
NONCE="234" # arbitrary

# Build the payload for token transfers. Declared on multiple lines to
# be more legible. Data pulled from an arbitrary LogMessagePublished event
# on etherscan. Metadata and fees commented out, leaving only the payload
PAYLOAD="0x"
declare -a SLOTS=(
# "0000000000000000000000000000000000000000000000000000000000055baf"
# "0000000000000000000000000000000000000000000000000000000000000000"
# "0000000000000000000000000000000000000000000000000000000000000080"
# "0000000000000000000000000000000000000000000000000000000000000001"
# "00000000000000000000000000000000000000000000000000000000000000ae"
"030000000000000000000000000000000000000000000000000000000005f5e1"
"000000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c5"
"9900020000000000000000000000000000000000000000000000000000000000"
"000816001000000000000000000000000044eca3f6295d6d559ca1d99a5ef5a8"
"f72b4160f10001010200c91f01004554480044eca3f6295d6d559ca1d99a5ef5"
"a8f72b4160f10000000000000000000000000000000000000000000000000000"
)
for i in "${SLOTS[@]}"
do
PAYLOAD="$PAYLOAD$i"
done

echo "DEBUG:"
echo "- RPC=${RPC}"
echo "- CORE_BRIDGE_CONTRACT=${CORE_BRIDGE_CONTRACT}"
echo "- TOKEN_BRIDGE_CONTRACT=${TOKEN_BRIDGE_CONTRACT}"
echo "- MNEMONIC=${MNEMONIC}"
echo "- FROM=${FROM}"
echo "- VALUE=${VALUE}"
echo "- RECIPIENT=${RECIPIENT}"
echo

# Fund the token bridge from the user
echo "Start impersonating Anvil key: ${ANVIL_USER}"
cast rpc \
anvil_impersonateAccount "${ANVIL_USER}" \
--rpc-url "${RPC}"
echo "Funding token bridge using the user's balance"
cast send --unlocked \
--rpc-url "${RPC}" \
--from $ANVIL_USER \
--value 100000000000000 \
${TOKEN_BRIDGE_CONTRACT}
echo ""
echo "End impersonating User0"
cast rpc \
anvil_stopImpersonatingAccount "${ANVIL_USER}" \
--rpc-url "${RPC}"

BALANCE_CORE=$(cast balance --rpc-url "${RPC}" $CORE_BRIDGE_CONTRACT)
BALANCE_TOKEN=$(cast balance --rpc-url "${RPC}" $TOKEN_BRIDGE_CONTRACT)
BALANCE_USER=$(cast balance --rpc-url "${RPC}" $ANVIL_USER)
echo "BALANCES:"
echo "- CORE_BRIDGE_CONTRACT=${BALANCE_CORE}"
echo "- TOKEN_BRIDGE_CONTRACT=${BALANCE_TOKEN}"
echo "- ANVIL_USER=${BALANCE_USER}"
echo

# === Malicious call to transferTokensWithPayload()
# This is the exploit scenario: the token bridge has called publishMessage() without a ERC20 Transfer or Deposit
# being present in the same receipt.
# This is done by impersonating the token bridge contract and sending a message directly to the core bridge.
# Ensure that anvil is using `--auto-impersonate` or else that account impersonation is enabled in your local environment.
# --private-key "$MNEMONIC" \
# --max-fee 500000 \
echo "Start impersonate token bridge"
cast rpc \
--rpc-url "${RPC}" \
anvil_impersonateAccount "${TOKEN_BRIDGE_CONTRACT}"
echo "Calling publishMessage as ${TOKEN_BRIDGE_CONTRACT}"
cast send --unlocked \
--rpc-url "${RPC}" \
--json \
--gas-limit 10000000 \
--priority-gas-price 1 \
--from "${TOKEN_BRIDGE_CONTRACT}" \
--value "0" \
"${CORE_BRIDGE_CONTRACT}" \
"publishMessage(uint32,bytes,uint8)" \
0 "${PAYLOAD}" 1
echo ""
cast rpc \
--rpc-url "${RPC}" \
anvil_stopImpersonatingAccount "${TOKEN_BRIDGE_CONTRACT}"
echo "End impersonate token bridge"

# TODO add the 'multicall' scenario encoded in the forge script

echo "Done Transfer Verifier integration test."
echo "Exiting."
32 changes: 32 additions & 0 deletions devnet/tx-verifier-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: batch/v1
kind: Job
metadata:
name: tx-verifier-test
spec:
# Number of successful pod completions needed
completions: 1
# Number of pods to run in parallel
parallelism: 1
# Time limit after which the job is terminated (optional)
# activeDeadlineSeconds: 100
# Number of retries before marking as failed
backoffLimit: 4
template:
metadata:
labels:
app: tx-verifier-test
spec:
restartPolicy: Never
containers:
- name: tx-verifier-test
image: tx-verifier-test
command:
- /bin/bash
- -c
- "/transfer-verifier-test.sh"
env:
- name: RPC_URL
value: "ws://eth-devnet:8545"
volumes:
- name: log-volume
emptyDir: {}
Loading
Loading