diff --git a/.gitignore b/.gitignore index 5b455b1b3..24cf00620 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,9 @@ bigtable-writer.json **/payer-mainnet.json **/payer-testnet.json **/payer-devnet.json +/solana/artifacts/ +/terra/artifacts/ +!/terra/artifacts/cw20_base.wasm +/solana/artifacts-testnet/ +/solana/artifacts-devnet/ +/solana/artifacts-mainnet/ diff --git a/Makefile b/Makefile index 4ddc6d669..8790c4a9a 100755 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ PREFIX ?= /usr/local OUT = build BIN = $(OUT)/bin +-include Makefile.help + VERSION = $(shell git describe --tags --dirty) .PHONY: dirs @@ -16,6 +18,7 @@ dirs: Makefile @mkdir -p $(BIN) .PHONY: install +## Install guardiand binary install: install -m 775 $(BIN)/* $(PREFIX)/bin setcap cap_ipc_lock=+ep $(PREFIX)/bin/guardiand @@ -28,6 +31,7 @@ generate: dirs tools/bin/buf generate .PHONY: node +## Build guardiand binary node: $(BIN)/guardiand .PHONY: $(BIN)/guardiand diff --git a/Tiltfile b/Tiltfile index 960d029bd..10c9846cb 100644 --- a/Tiltfile +++ b/Tiltfile @@ -252,6 +252,8 @@ if solana: ref = "solana-contract", context = "solana", dockerfile = "solana/Dockerfile", + target = "builder", + build_args = {"BRIDGE_ADDRESS": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"} ) # solana local devnet diff --git a/solana/Dockerfile b/solana/Dockerfile index f8696773f..78fd711da 100644 --- a/solana/Dockerfile +++ b/solana/Dockerfile @@ -1,5 +1,5 @@ #syntax=docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95912df1cf7d313c3e2230a333fdbcc -FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965bc1ed43b607871b1f362e06a5 +FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965bc1ed43b607871b1f362e06a5 AS builder RUN apt-get update && \ apt-get install -y \ @@ -35,7 +35,8 @@ ADD . . RUN mkdir -p /opt/solana/deps ENV EMITTER_ADDRESS="11111111111111111111111111111115" -ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" +ARG BRIDGE_ADDRESS +RUN [ -n "${BRIDGE_ADDRESS}" ] # Build Wormhole Solana programs RUN --mount=type=cache,target=bridge/target \ @@ -56,3 +57,6 @@ RUN --mount=type=cache,target=bridge/target \ ENV RUST_LOG="solana_runtime::system_instruction_processor=trace,solana_runtime::message_processor=trace,solana_bpf_loader=debug,solana_rbpf=debug" ENV RUST_BACKTRACE=1 + +FROM scratch AS export-stage +COPY --from=builder /opt/solana/deps / diff --git a/solana/Makefile b/solana/Makefile new file mode 100644 index 000000000..b1bacfadb --- /dev/null +++ b/solana/Makefile @@ -0,0 +1,76 @@ +# Mainnet buffer authority is the "upgrade" PDA +bridge_ADDRESS_mainnet=worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth +bridge_AUTHORITY_mainnet=2rCAC1VKz5YP1jZTHcVfWDhHMs2iEruUaATdeZe5Fjk5 +token_bridge_ADDRESS_mainnet=wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb +token_bridge_AUTHORITY_mainnet=DHyAcRbFpRWTkcsAsfwQpbABXvtjs6bQ1dq5ScNhRDoQ +nft_bridge_ADDRESS_mainnet=WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD +nft_bridge_AUTHORITY_mainnet=3cVZHphy4QUYnU1hYFyvHF9joeZJ6ZTxpWx1nzavaUa8 + +# Testnet buffer authority is the deployer public key +bridge_ADDRESS_testnet=3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5 +bridge_AUTHORITY_testnet=9r6q2iEg4MBevjC8reaLmQUDxueF3vabUoqDkZ2LoAYe +token_bridge_ADDRESS_testnet=DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe +token_bridge_AUTHORITY_testnet=9r6q2iEg4MBevjC8reaLmQUDxueF3vabUoqDkZ2LoAYe +nft_bridge_ADDRESS_testnet=2rHhojZ7hpu1zA91nvZmT8TqWWvMcKmmNBCr2mKTtMq4 +nft_bridge_AUTHORITY_testnet=9r6q2iEg4MBevjC8reaLmQUDxueF3vabUoqDkZ2LoAYe + +# Devnet buffer authority is the devnet public key +bridge_ADDRESS_devnet=Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o +bridge_AUTHORITY_devnet=6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J +token_bridge_ADDRESS_devnet=B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE +token_bridge_AUTHORITY_devnet=6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J +nft_bridge_ADDRESS_devnet=NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA +nft_bridge_AUTHORITY_devnet=6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J + +SOURCE_FILES=$(shell find . -name "*.rs" -or -name "*.lock" -or -name "*.toml" | grep -v "target") Dockerfile + +.PHONY: clean all help artifacts deploy/bridge deploy/token_bridge deploy/nft_bridge .FORCE + +-include ../Makefile.help + +.FORCE: + +## Build contracts. +artifacts: check-network artifacts-$(NETWORK) + +.PHONY: check-network +check-network: +ifndef bridge_ADDRESS_$(NETWORK) + $(error Invalid or missing NETWORK. Please call with `$(MAKE) $(MAKECMDGOALS) NETWORK=[mainnet | testnet | devnet]`) +endif + +artifacts-$(NETWORK): $(SOURCE_FILES) + echo $@ + @echo "Building artifacts for ${NETWORK} (${bridge_ADDRESS_${NETWORK}})" + DOCKER_BUILDKIT=1 docker build -f Dockerfile --build-arg BRIDGE_ADDRESS=${bridge_ADDRESS_${NETWORK}} -o $@ . + cd $@ && ls | xargs sha256sum > checksums.txt + +payer-$(NETWORK).json: + $(error Missing private key in payer-$(NETWORK).json) + +%-buffer-$(NETWORK).txt: payer-$(NETWORK).json check-network + $(eval FLAG := $(shell echo $(if $(filter mainnet,$(NETWORK)), m,\ + $(if $(filter testnet,$(NETWORK)), d,\ + l\ + )))) + solana -k payer-${NETWORK}.json program write-buffer artifacts-$(NETWORK)/$*.so -u $(FLAG) | cut -f2 -d' ' > $@ + solana -k payer-${NETWORK}.json program set-buffer-authority $$(cat $@) --new-buffer-authority $($*_AUTHORITY_$(NETWORK)) -u $(FLAG) + +## Deploy core bridge program. +deploy/bridge: bridge-buffer-$(NETWORK).txt + @echo Deployed core bridge contract at: + @cat $< + +## Deploy token bridge program. +deploy/token_bridge: token_bridge-buffer-$(NETWORK).txt + @echo Deployed token bridge contract at: + @cat $< + +## Deploy nft bridge program. +deploy/nft_bridge: nft_bridge-buffer-$(NETWORK).txt + @echo Deployed nft bridge contract at: + @cat $< + +clean: + rm -rf artifacts-mainnet artifacts-testnet artifacts-devnet *-buffer-*.txt + diff --git a/solana/README.md b/solana/README.md new file mode 100644 index 000000000..6f6df7eca --- /dev/null +++ b/solana/README.md @@ -0,0 +1,111 @@ +# Solana Wormhole Contract Deployment + +This readme describes the steps for building, verifying, and deploying Solana smart contracts for Wormhole. + +**WARNING**: *This process is only Linux host compatible at this time.* + +## Verify Tilt + +Before building Solana contracts, ensure that the specific commit you will be building from passes in tilt. This that ensures basic functionality of the Solana smart contracts that you are about to build and deploy. + + +## Build Contracts + +The following command can be used to build contracts for Solana contracts via Docker. + +Build Target Options: [`mainnet`|`testnet`|`devnet`| + +These network names correspond to the naming convention used by wormhole +elsewhere. This means that `mainnet` corresponds to Solana `mainnet-beta`, +`testnet` corresponds to Solana `devnet`, and `devnet` is Solana `localhost`. + +```console +wormhole/solana $ make NETWORK=BUILD_TARGET artifacts +``` +Example: `make NETWORK=testnet artifacts` + + +Upon completion, the compiled bytecode for the Solana contracts will be placed into an artifacts directory with a convention of `artifacts-BUILD_TARGET` (eg. `artifacts-testnet`) + +The contract addresses are compiled into the binaries, which is why these build +outputs are kept separate. The deploy script below makes sure that only the +right binaries can be deployed to each network. + +You may set the build target in the `NETWORK` environment variable, and then +omit it from all of the subsequent commands. +Example: +```console +export NETWORK=testnet +make artifacts +``` + +## Verify Checksums + +Now that you have built the Solana Contracts, you should ask a peer to build using the same process and compare the equivalent checksums.txt files to make sure the contract bytecode(s) are deterministic. + +Verify Target Options: [`mainnet`|`testnet`|`devnet`] + +```console +wormhole/solana $ cat artifacts-VERIFY_TARGET/checksums.txt +``` +Example: `cat artifacts-testnet/checksums.txt` + + +Once you have verified the Solana contracts are deterministic with a peer, you can now move to the deploy step. + +## Deploy Contracts + +Now that you have built and verified checksums, you can now deploy one or more relevant contracts to the Solana blockchain. + +Deploy Target Options: [`mainnet`|`testnet`|`devnet`] + +You will need to define a `payer-DEPLOY_TARGET.json` for the relevant deploy target (eg. `payer-testnet.json`). This will contain the relevant wallet private key that you will be using to deploy the contracts. + +```console +wormhole/solana $ make NETWORK=DEPLOY_TARGET deploy/bridge +wormhole/solana $ make NETWORK=DEPLOY_TARGET deploy/token_bridge +wormhole/solana $ make NETWORK=DEPLOY_TARGET deploy/nft_bridge +``` +Example: `make NETWORK=testnet deploy/bridge` + +For each deployed contract, you will get an account address for that relevant account address for the deployment, make note of these so you can use them in the next step for on-chain verification. + +## Verify On-Chain + +Now that you have deployed one or more contracts on-chain, you can verify the onchain bytecode and make sure it matches the same checksums you identified above. + +For each contract you wish to verify on-chain, you will need the following elements: + +- Path to the contracts bytecode (eg. `artifacts-testnet/bridge.so`) +- Solana account address for the relevant contract (eg. `9GRbmSbdrWGNf9z27YrhPbWnL7zZ3doeQAq2LrkmCB4Y`) +- A network to verify on (`mainnet`, `testnet`, or `devnet`) + +Below is how to verify all three contracts on testnet: + +```console +wormhole/solana $ verify -n testnet artifacts-testnet/bridge.so NEW_BRIDGE_ACCOUNT_ADDRESS +wormhole/solana $ verify -n testnet artifacts-testnet/token_bridge.so NEW_TOKEN_BRIDGE_ACCOUNT_ADDRESS +wormhole/solana $ verify -n testnet artifacts-testnet/nft_bridge.so NEW_NFT_BRIDGE_ACCOUNT_ADDRESS +``` +Example: `verify -n testnet artifacts-testnet/bridge.so 9GRbmSbdrWGNf9z27YrhPbWnL7zZ3doeQAq2LrkmCB4Y` + +Again, if you have the NETWORK environment variable set, then the `-n` flag is optional: +```console +verify artifacts-testnet/bridge.so 9GRbmSbdrWGNf9z27YrhPbWnL7zZ3doeQAq2LrkmCB4Y +``` + +For each contract, you should expect a `Successfully verified` output message. If all contracts can be successfully verified, you can engage in Wormhole protocol governance to obtain an authorized VAA for the contract upgrade(s). + +A verification failure should never happen, and is a sign of some error in the deployment process. + +If you instead get `Failed to verify` for any contract, you can debug the differences between the compiled and on-chain byte code using `hashdump` and `vimdiff`. The `verify` script temporarily caches bytecode in `/tmp/account.dump`, so you can run the verify command for a contract that doesn't verify and then debug as needed. + +Below is an example of the commands you'd need to `hexdump` and `vimdiff` the differences in contract bytecode: + +```console +wormhole/solana $ hexdump -C /tmp/account.dump > deployed.bin +wormhole/solana $ hexdump -C artifacts-testnet/bridge.so > compiled.bin +wormhole/solana $ vimdiff compiled.bin deployed.bin +``` + +Do not proceed with governance until you can verify the on-chain bytecode with the locally compiled bytecode. diff --git a/solana/bridge/Makefile b/solana/bridge/Makefile new file mode 100644 index 000000000..75278186e --- /dev/null +++ b/solana/bridge/Makefile @@ -0,0 +1,33 @@ +BRIDGE_ADDRESS=Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o +# These tests use a different emitter for some reason +export EMITTER_ADDRESS=CiByUvEcx7w2HA4VHcPCBUAFQ73Won9kB36zW9VjirSr + +.PHONY: test .FORCE + +.FORCE: + +target/deploy/bridge.so: .FORCE + cargo build-bpf + +/tmp/ledger: + mkdir -p $@ + +test: target/deploy/bridge.so /tmp/ledger +# Kill solana test validator if already running, as we'll need a new one + @if pgrep solana-test; then pkill solana-test; fi +# Sleep here for a bit so that the process can close the port + @sleep 1 +# Start test validator +# We redirect the logs to test.log in case they're useful + solana-test-validator \ + --ledger /tmp/ledger \ + --bpf-program ${BRIDGE_ADDRESS} target/deploy/bridge.so \ + --reset --log > test.log 2>&1 & +# Sleep for a few seconds so the validator initialises properly + @sleep 5 +# Run tests. If the tests fail, we kill the validator, but make sure we return +# status 1 (for CI) + BRIDGE_PAYER=/tmp/ledger/faucet-keypair.json cargo test || (pkill solana-test && exit 1) +# If the tests succeeded, let's kill the validator + @pkill solana-test + diff --git a/solana/verify b/solana/verify new file mode 100755 index 000000000..1e3c897bf --- /dev/null +++ b/solana/verify @@ -0,0 +1,71 @@ +#!/bin/bash + +set -euo pipefail + +usage="Usage: + $(basename "$0") <.so file> [-h] [-n network] -- Verify that the deployed on-chain bytecode matches the local object file + + where: + -h show this help text + -n set the network (mainnet, testnet, devnet. defaults to \$NETWORK if set)" + +network=$NETWORK +while getopts ':hn:' option; do + case "$option" in + h) echo "$usage" + exit + ;; + n) network=$OPTARG + ;; + :) printf "missing argument for -%s\n" "$OPTARG" >&2 + echo "$usage" >&2 + exit 1 + ;; + \?) printf "illegal option: -%s\n" "$OPTARG" >&2 + echo "$usage" >&2 + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +case "$network" in + mainnet) moniker="m";; + testnet) moniker="d";; + devnet) moniker="l";; + *) printf "Network not set. Specify with -n\n" >&2 + echo "$usage" >&2 + exit 1 + ;; +esac + +[ $# -ne 2 ] && { echo "$usage" >&2; exit 1; } +obj_file=$1 +sol_addr=$2 + +# Grab account content as JSON +solana account $sol_addr -u $moniker --output-file /tmp/account.json --output json-compact >/dev/null +# decode the base64 account data to binary +cat /tmp/account.json | jq '.account.data[0]' | sed s/\"//g | base64 -d > /tmp/account.dump + +# The first 37 bytes are irrelevant, the actual ELF object code starts after, +# so we drop these bytes. Presumably those bytes correspond to an encoded rust +# enum constructor? +# Set the block size to 37 bytes and skip the first block. +dd bs=37 skip=1 if=/tmp/account.dump of=/tmp/bytecode.dump 2>/dev/null + +hash1=`sha256sum /tmp/bytecode.dump | cut -f1 -d' '` +hash2=`sha256sum $obj_file | cut -f1 -d' '` + +echo "Deployed bytecode hash (on $network):" +echo $hash1 +echo "$obj_file hash:" +echo $hash2 + +if [ "$hash1" == "$hash2" ]; then + printf "\033[0;32mSuccessfully verified\033[0m\n"; + exit 0; +else + printf "\033[0;31mFailed to verify\033[0m\n"; + exit 1; +fi