From d335c11eb614a95d234db05ed7afb575d8aa8ce6 Mon Sep 17 00:00:00 2001 From: Marcel Moura Date: Fri, 17 Nov 2023 18:52:37 -0300 Subject: [PATCH] test: generate devnet for local testing --- scripts/devnet/gen-devnet.sh | 363 ++++++++++++++++++++++++++++++++ scripts/devnet/lib/anvil.sh | 63 ++++++ scripts/devnet/lib/contracts.sh | 168 +++++++++++++++ scripts/devnet/lib/util.sh | 30 +++ scripts/devnet/remappings.txt | 2 + 5 files changed, 626 insertions(+) create mode 100755 scripts/devnet/gen-devnet.sh create mode 100755 scripts/devnet/lib/anvil.sh create mode 100644 scripts/devnet/lib/contracts.sh create mode 100644 scripts/devnet/lib/util.sh create mode 100644 scripts/devnet/remappings.txt diff --git a/scripts/devnet/gen-devnet.sh b/scripts/devnet/gen-devnet.sh new file mode 100755 index 000000000..138ab2f2f --- /dev/null +++ b/scripts/devnet/gen-devnet.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +# (c) Cartesi and individual authors (see AUTHORS) +# SPDX-License-Identifier: Apache-2.0 (see LICENSE) +set -o nounset +set -o pipefail +if [[ "${TRACE-0}" == "1" ]]; then set -o xtrace; fi + +script_dir="$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly script_dir + +. "$script_dir/lib/util.sh" +. "$script_dir/lib/anvil.sh" +. "$script_dir/lib/contracts.sh" + +################################################################################ +# Configuration +ROLLUPS_CONTRACTS_VERSION="1.1.0" +DEVNET_RPC_URL="http://localhost:8545" +DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +DEVNET_FOUNDRY_ACCOUNT_0_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +# keccak256("salt") +DEVNET_DEFAULT_SALT="0xa05e334153147e75f3f416139b5109d1179cb56fef6a4ecb4c4cbc92a7c37b70" +readonly ROLLUPS_CONTRACTS_VERSION \ + DEVNET_RPC_URL \ + DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS \ + DEVNET_FOUNDRY_ACCOUNT_0_PRIVATE_KEY \ + DEVNET_DEFAULT_SALT + +# Defaults +devnet_anvil_state_file=$(realpath "./anvil_state.json") +devnet_deployment_file=$(realpath "./deployment.json") +template_hash_file="" +VERBOSE="" +forge_remappings_file="$script_dir/remappings.txt" +readonly forge_remappings_file + +# Deployment info, which will be gathered during processing +declare -A deployment_info + +################################################################################ +# Utility functions +################################################################################ +usage() +{ + echo "Generate devnet for testing the Cartesi Rollups Node" + echo + echo "Usage: $0 [options]" + echo + echo "OPTIONS:" + echo + echo " -t template-hash-file" + echo " Mandatory Cartesi Machine template hash file" + echo " -a" + echo " Path for output anvil state file" + echo " -d" + echo " Path for deployment information file" + echo " -v" + echo " Verbose mode" + echo " -h" + echo " Show this message" + echo + + exit 0 +} + +################################################################################ +# Finish processing and clean up environment +finish() { + verbose "finishing up" + + if [ -n "$work_dir" ]; then + rm -rf "$work_dir" + check_error $? "failed to remove $work_dir" + verbose "removing $work_dir" + fi + + if [[ -n "$anvil_pid" ]]; then + anvil_down "$anvil_pid" + fi +} + +################################################################################ +# Print deployment report +print_report() { + log "Deployment report:" + log "rollups-contracts version: $ROLLUPS_CONTRACTS_VERSION" + log "anvil state file location: $devnet_anvil_state_file" + log "deployment file location : $devnet_deployment_file" +} + +################################################################################ +# Generate deployment file +generate_deployment_file() { + local deployment_file="$1" + + echo "{" > "$deployment_file" + for key in "${!deployment_info[@]}"; do + echo " \"${key}\": \"${deployment_info[${key}]}\"," >> "$deployment_file" + done + echo "}" >> "$deployment_file" + verbose "Deployment information saved to $deployment_file" +} + +################################################################################ +# Deploy rollups libraries +deploy_libraries() { + local contract="lib/@cartesi/util/contracts/CartesiMathV2.sol" + local name="CartesiMathV2" + local lib_address="" + contract_deploy \ + lib_address \ + "$contract" \ + "$name" + mathv2_lib="$contract:$name:$lib_address" + verbose "deployed $mathv2_lib" + + contract="lib/@cartesi/util/contracts/MerkleV2.sol" + name="MerkleV2" + contract_deploy \ + lib_address \ + "$contract" \ + "$name" \ + --libraries \ + "$mathv2_lib" + merklev2_lib="$contract:$name:$lib_address" + verbose "deployed $merklev2_lib" + + contract="lib/@cartesi/util/contracts/Bitmask.sol" + name="Bitmask" + contract_deploy \ + lib_address \ + "$contract" \ + "$name" + bitmask_lib="$contract:$name:$lib_address" + verbose "deployed $bitmask_lib" +} + +################################################################################ +# Deploy portals +deploy_portals() { + local name="InputBox" + local contract="src/inputs/InputBox.sol" + local portal_address="" + contract_deploy \ + portal_address \ + "$contract" \ + "$name" + deployment_info["CARTESI_CONTRACTS_INPUT_BOX_ADDRESS"]="$portal_address" + verbose "deployed $contract:$name:$portal_address" + + declare -A portals + portals["ERC1155BatchPortal"]="src/portals/ERC1155BatchPortal.sol" + portals["ERC1155SinglePortal"]="src/portals/ERC1155SinglePortal.sol" + portals["ERC20Portal"]="src/portals/ERC20Portal.sol" + portals["ERC721Portal"]="src/portals/ERC721Portal.sol" + portals["EtherPortal"]="src/portals/EtherPortal.sol" + + local inputbox_address="$portal_address" + for key in "${!portals[@]}"; do + name="${key}" + contract="${portals[${key}]}" + contract_deploy \ + portal_address \ + "$contract" \ + "$name" \ + --constructor-args \ + "$inputbox_address" + verbose "deployed $contract:$name:$portal_address" + done +} + +################################################################################ +# Deploy DAppRelay +deploy_relay() { + local contract="src/relays/DAppAddressRelay.sol" + local name="DAppAddressRelay" + local relay_address="" + contract_deploy \ + relay_address \ + "$contract" \ + "$name" \ + --constructor-args \ + "$DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS" + verbose "deployed $contract:$name:$relay_address" +} + +################################################################################ +# Create DApp factories +create_factories() { + local -n dapp_factory_addr="$1" + shift + local -n auth_hist_factory_addr="$1" + shift + + local contract="src/dapp/CartesiDAppFactory.sol" + local name="CartesiDAppFactory" + local factory_address="" + contract_deploy \ + factory_address \ + "$contract" \ + "$name" \ + --libraries "$mathv2_lib" \ + --libraries "$merklev2_lib" \ + --libraries "$bitmask_lib" + dapp_factory_addr="$factory_address" + verbose "deployed $contract:$name:$factory_address" + + contract="src/consensus/authority/AuthorityFactory.sol" + name="AuthorityFactory" + contract_deploy \ + factory_address \ + "$contract" \ + "$name" \ + --constructor-args \ + "$DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS" + verbose "deployed $contract:$name:$factory_address" + authority_factory_address="$factory_address" + + contract="src/history/HistoryFactory.sol" + name="HistoryFactory" + contract_deploy \ + factory_address \ + "$contract" \ + "$name" \ + --constructor-args \ + "$DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS" + verbose "deployed $contract:$name:$factory_address" + history_factory_address="$factory_address" + + contract="src/consensus/authority/AuthorityHistoryPairFactory.sol" + name="AuthorityHistoryPairFactory" + contract_deploy \ + factory_address \ + "$contract" \ + "$name" \ + --constructor-args \ + "$authority_factory_address" \ + "$history_factory_address" + auth_hist_factory_addr="$factory_address" + verbose "deployed $contract:$name:$factory_address" +} + +################################################################################ +# create DApp contracts +create_dapp() { + local -n ret="$1" + shift + factory_addr="$1" + shift + dapp_factory_addr="$1" + shift + + local addresses + contract_create \ + addresses \ + block_number \ + "$factory_addr" \ + "newAuthorityHistoryPair(address,bytes32)(address,address)" \ + "$DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS" \ + "$DEVNET_DEFAULT_SALT" + authority_address="${addresses[0]}" + history_address="${addresses[1]}" + deployment_info["CARTESI_CONTRACTS_AUTHORITY_ADDRESS"]="$authority_address" + deployment_info["CARTESI_CONTRACTS_HISTORY_ADDRESS"]="$history_address" + verbose "deployed authority_address=$authority_address" + verbose "deployed history_address=$history_address" + + contract_create \ + addresses \ + block_number \ + "$dapp_factory_addr" \ + "newApplication(address,address,bytes32,bytes32)(address)" \ + "$authority_address" \ + "$DEVNET_FOUNDRY_ACCOUNT_0_ADDRESS" \ + "$template_hash" \ + "$DEVNET_DEFAULT_SALT" + ret="${addresses[0]}" + deployment_info["CARTESI_CONTRACTS_DAPP_ADDRESS"]="$ret" + deployment_info["CARTESI_CONTRACTS_DAPP_DEPLOYMENT_BLOCK_NUMBER"]="$block_number" + verbose "deployed dapp_address=$ret" +} + +################################################################################ +# Main workflow +################################################################################ + +# Process script options +while getopts ":a:d:t:hv" option; do + case $option in + a) + devnet_anvil_state_file=$(realpath "$OPTARG") + ;; + d) + devnet_deployment_file=$(realpath "$OPTARG") + ;; + t) + template_hash_file="$OPTARG" + ;; + h) + usage + ;; + v) + VERBOSE=1 + ;; + \?) + err "$OPTARG is not a valid option" + usage + ;; + esac +done + +if [[ -z "$template_hash_file" ]]; then + err "missing template-hash-file" + usage +fi + +template_hash=$(xxd -p "$template_hash_file") +check_error $? "failed to read template hash" +template_hash=$(echo "$template_hash" | tr -d "\n") +readonly devnet_anvil_state_file devnet_deployment_file template_hash + +# From here on, any exit deserves a clean up +trap finish EXIT ERR + +log "starting devnet creation" +work_dir=$(mktemp -d) +readonly work_dir +check_error $? "faile to create temp dir" +verbose "created $work_dir" + +anvil_pid="" +anvil_up \ + anvil_pid\ + "$devnet_anvil_state_file" +check_error $? "failed to start anvil" +log "started anvil (pid=$anvil_pid)" + +forge_prepare \ + "$work_dir" \ + "$forge_remappings_file" +log "prepared forge environment" + +deploy_libraries +deploy_portals +deploy_relay +log "deployed contracts" + +create_factories \ + dapp_factory_address \ + auth_hist_factory_address +log "created factories" + +create_dapp \ + dapp_address \ + "$auth_hist_factory_address" \ + "$dapp_factory_address" +log "created CartesiDApp" + +generate_deployment_file \ + "$devnet_deployment_file" + +print_report +log "done creating devnet" diff --git a/scripts/devnet/lib/anvil.sh b/scripts/devnet/lib/anvil.sh new file mode 100755 index 000000000..3a3bd3984 --- /dev/null +++ b/scripts/devnet/lib/anvil.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# (c) Cartesi and individual authors (see AUTHORS) +# SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +readonly DEVNET_ANVIL_IP="0.0.0.0" +readonly DEVNET_ANVIL_TIMEOUT=3 + +_is_anvil_up() { + local -n ready=$1 + + local result="$( + curl -X \ + POST \ + -s \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":"1","method":"net_listening","params":[]}' \ + "http://$DEVNET_ANVIL_IP:8545" + )" + + ready="false" + if [[ -n "$result" ]]; then + ready=$(echo "$result" | jq ".result") + fi +} + +anvil_up() { + local -n ret="$1" + shift + local anvil_state_file="$1" + + # check if there's another instance of anvil up listening on the same port + _is_anvil_up is_up + if [[ "$is_up" == "true" ]]; then + err "anvil is already up" + return 1 + fi + + anvil \ + --host $DEVNET_ANVIL_IP \ + --dump-state "$anvil_state_file" \ + > /dev/null & + local pid=$! + + # check if anvil is up + sleep $DEVNET_ANVIL_TIMEOUT + _is_anvil_up is_up + if [[ "$is_up" != "true" ]]; then + err "anvil has not started" + return 2 + fi + + ret="$pid" +} + +anvil_down() { + local anvil_pid="$1" + + verbose "waiting $DEVNET_ANVIL_TIMEOUT seconds before killing anvil..." + sleep $DEVNET_ANVIL_TIMEOUT + kill "$anvil_pid" + check_error $? "failed to kill anvil" + verbose "killed anvil (pid=$anvil_pid)" +} diff --git a/scripts/devnet/lib/contracts.sh b/scripts/devnet/lib/contracts.sh new file mode 100644 index 000000000..56328986e --- /dev/null +++ b/scripts/devnet/lib/contracts.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# (c) Cartesi and individual authors (see AUTHORS) +# SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +################################################################################ +# Download rollups contracts +_download() { + local download_dir=$1 + shift + local artifact_name=$1 + shift + local artifact_version=$1 + + local tgz_file="$artifact_name-$artifact_version.tgz" + local url="https://registry.npmjs.org/@cartesi/$artifact_name/-/$tgz_file" + wget \ + --quiet \ + $url \ + --directory-prefix $download_dir + check_error $? "failed to download $url" + + local tar_dir="$download_dir/$artifact_name-$artifact_version" + mkdir -p $tar_dir + tar zxf \ + $download_dir/$tgz_file \ + --directory $tar_dir \ + > /dev/null + echo $tar_dir +} + +rollups_download() { + local download_dir=$1 + shift + local contracts_version=$1 + + _download $download_dir "rollups" $contracts_version +} + +solidity_util_download() { + local download_dir=$1 + shift + local rollups_contracts_dir=$1 + + version=$(\ + cat ${rollups_contracts_dir}/package/package.json \ + | jq -r '.dependencies."@cartesi/util"' \ + ) + + _download $download_dir "util" $version +} + +################################################################################ +# Prepare forge environment +forge_prepare() { + local tmp_dir=$1 + shift + local remappings_file="$1" + + cd "$tmp_dir" + mkdir forge_prj + cd forge_prj + + # download contracts + local download_dir="$tmp_dir/downloads" + mkdir -p "$download_dir" + local rollups_tar_dir=$( + rollups_download \ + "$download_dir" \ + "$ROLLUPS_CONTRACTS_VERSION" + ) + check_error $? "failed to download contracts" + log "downloaded rollups-contracts to $rollups_tar_dir" + + local solidity_util_tar_dir=$( + solidity_util_download \ + "$download_dir" \ + "$rollups_tar_dir" + ) + check_error $? "failed to download solidity-util" + log "downloaded solidity-util to $solidity_util_tar_dir" + + forge init \ + --shallow \ + --no-git \ + --quiet \ + . &> /dev/null + check_error $? "failed to init forge project" + + # install openzeppelin + openzeppelin_version=v$(\ + cat ${rollups_tar_dir}/package/package.json \ + | jq -r '.dependencies."@openzeppelin/contracts"' \ + ) + forge install \ + --shallow \ + --no-git \ + --quiet \ + openzeppelin/openzeppelin-contracts@${openzeppelin_version} \ + &> /dev/null + check_error $? "failed to install openzeppelin $openzeppelin_version" + + # copy contracts + local rollups_contracts_dir="$rollups_tar_dir/package/contracts" + cp -pr $rollups_contracts_dir/* src + + local solidity_util_contracts_dir="$solidity_util_tar_dir/package/contracts" + local util_lib_dir="lib/@cartesi/util/contracts" + mkdir -p $util_lib_dir + cp -pr $solidity_util_contracts_dir/* $util_lib_dir + + # apply forge remappings + cp "$remappings_file" "remappings.txt" +} + +################################################################################ +# Deploy a contract +# Assumes all non-standard arguments are passed +# output: returned address +# input: contract and dependencies +contract_deploy() { + local -n address="$1" + shift + local contract="$1" + shift + local contract_name="$1" + shift + + address=$( + forge create \ + --json \ + --rpc-url $DEVNET_RPC_URL \ + --private-key $DEVNET_FOUNDRY_ACCOUNT_0_PRIVATE_KEY \ + "$contract:$contract_name" \ + $@ \ + | jq -r ".deployedTo" + ) + check_error $? "failed to deploy $contract_name" +} + +################################################################################ +# Call arbitrary code on a contract +# Splits returned values into an array +contract_create() { + local -n addrs="$1" + shift + local -n block="$1" + shift + + # Generate values without issuing a transaction + local values=$( + cast call \ + --rpc-url $DEVNET_RPC_URL \ + $@ + ) + check_error $? "failed to retrieve returned values" + # Split returned values + IFS=$'\n' addrs=($values) + + # Send tansaction + block=$(cast send \ + --json \ + --rpc-url $DEVNET_RPC_URL \ + --private-key $DEVNET_FOUNDRY_ACCOUNT_0_PRIVATE_KEY \ + $@ \ + | jq -r '.blockNumber' + ) + check_error $? "failed to send transaction" +} diff --git a/scripts/devnet/lib/util.sh b/scripts/devnet/lib/util.sh new file mode 100644 index 000000000..757ab3221 --- /dev/null +++ b/scripts/devnet/lib/util.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# (c) Cartesi and individual authors (see AUTHORS) +# SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +################################################################################ +# Utilitary functions +log() { + echo "$1" +} + +err() { + log "ERROR: $1" >&2 +} + +verbose() { + if [[ -n "$VERBOSE" ]]; then + log "$1" + fi +} + +check_error() { + exit_code=${1:-0} + shift + context=${1:-"main"} + + if [[ $exit_code -ne 0 ]]; then + err "$context: exit_code=$exit_code" + exit $exit_code + fi +} diff --git a/scripts/devnet/remappings.txt b/scripts/devnet/remappings.txt new file mode 100644 index 000000000..6d2a0c46f --- /dev/null +++ b/scripts/devnet/remappings.txt @@ -0,0 +1,2 @@ +@cartesi/=lib/@cartesi +@openzeppelin/=lib/openzeppelin-contracts/