diff --git a/HACKING.md b/HACKING.md index 7adbb8e..4df9d55 100644 --- a/HACKING.md +++ b/HACKING.md @@ -63,3 +63,20 @@ dfx canister install signer --mode upgrade --upgrade-unchanged --network staging ``` If you are not a controller, you may request a canister upgrade via Orbit. Please contact Leon Tan for the latest Orbit deployment instructions. + +## Deploy to Production + +- Create a GitHub release with a tag such as `v0.1.2` + - Update the GitHub release text. It is recommended to ask the team to review the text. + - Ensure that the release has been published. +- Check out the release commit +- Delete any old release directory. +- Install the corresponding `ic-admin`: `./scripts/setup ic-admin` +- Run: `./scripts/proposal-assets -t $TAG` + - Verify that `release/ci` contains the release Wasm and arguments. + - Run a docker build locally and verify that the Wasm and argument file hashes match. +- Run: `./scripts/proposal-template -t $TAG` + - Verify that `release/PROPOSAL.md` has been created. +- Run: `./scripts/propose` + - Verify the proposal very carefully, then submit the proposal. +- Create an appointment with trusted neurons to vote on the proposal. diff --git a/scripts/clap.bash b/scripts/clap.bash index 8a01990..ea6debf 100644 --- a/scripts/clap.bash +++ b/scripts/clap.bash @@ -1,5 +1,7 @@ # clap - a BASH argument parser # This parser aims to have similar parsing semantics as Rust's clap parser; if it doesn't look anything like clap it's not just you. +# +# Minimum supported bash version: 5 clap_usage="" clap_flag_match="" diff --git a/scripts/propose b/scripts/propose new file mode 100755 index 0000000..d5c4668 --- /dev/null +++ b/scripts/propose @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail +SOURCE_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +PATH="$SOURCE_DIR:$PATH" + +print_help() { + cat <<-EOF + + Creates an NNS proposal to upgrade the chain-fusion-signer. + + EOF +} + +# Source the clap.bash file --------------------------------------------------- +source "$SOURCE_DIR/clap.bash" +# Define options +clap.define short=n long=network desc="The dfx network to use" variable=DFX_NETWORK default="ic" +clap.define short=u long=nns_url desc="The NNS_URL; typically a network provider" variable=DFX_NNS_URL +clap.define short=N long=neuron desc="The neuron ID to propose with" variable=DFX_NEURON_ID +clap.define short=k long=hsm_key_id desc="The HSM key ID" variable=DFX_HSM_KEY_ID default="" +clap.define short=i long=identity desc="The dfx identity to use" variable=DFX_IDENTITY default="$(dfx identity whoami)" +clap.define long=save-proposal-id-to-file desc="Create a file with this name containing the proposal ID" variable=PROPOSAL_ID_FILE default="proposal_id.txt" +# Source the output file ---------------------------------------------------------- +source "$(clap.build)" + +cd "${SOURCE_DIR}/.." +echo "Preparing release for network: $DFX_NETWORK" + +# Gets the neuron ID. +# ... if saved in a file, get it: +NEURON_PATH="$HOME/.config/dfx/prod-neuron" +if test -e "$NEURON_PATH"; then + DFX_NEURON_ID="$(cat "$NEURON_PATH")" + echo "Neuron ID: $DFX_NEURON_ID (from $NEURON_PATH)" +else + read -rp "Please enter your Neuron ID: " DFX_NEURON_ID + read -rp "... Store the Neuron ID for future use? (y/N)" STORE_NEURON + [[ "$STORE_NEURON" != [yY] ]] || { + mkdir -p "$(dirname "$NEURON_PATH")" + printf "%s" "$DFX_NEURON_ID" >"$NEURON_PATH" + } +fi +[[ "${DFX_NEURON_ID:-}" != "" ]] || { + echo "ERROR: Neuron ID not provided." + exit 1 +} >&2 + +# Gets the HSM key id, if applicable +# ... for mainnet we use an HSM and the key ID in the standard setup is 01: +[[ "${DFX_HSM_KEY_ID:-}" != "" ]] || { + if [[ "${DFX_NETWORK:-}" =~ mainnet|ic ]]; then + DFX_HSM_KEY_ID="01" + fi +} +# ... else look for an HSM in the dfx identity: +[[ "${DFX_HSM_KEY_ID:-}" != "" ]] || { + DFX_HSM_KEY_ID="$(jq -re '.hsm.key_id // empty' "${HOME}/.config/dfx/identity/${DFX_IDENTITY}/identity.json")" || true +} +# ... else the neuron should be controlled by a pem file. + +# Gets the authentication mechanism +PEM_PATH="$HOME/.config/dfx/identity/${DFX_IDENTITY}/identity.pem" +if [[ "${DFX_NETWORK:-}" =~ mainnet|ic ]] || [[ "${DFX_HSM_KEY_ID:-}" != "" ]]; then + # Authenticate with an HSM + read -rp "HSM Pin: " DFX_HSM_PIN + export DFX_HSM_PIN # Needed for dfx canister id, for some strange reason. + AUTH_ARGS=(--use-hsm --pin "$DFX_HSM_PIN" --key-id "${DFX_HSM_KEY_ID}" --slot 0) +elif test -e "$PEM_PATH"; then + # Authenticate with a pem file + [[ "$(jq .encryption "$HOME/.config/dfx/identity/${DFX_IDENTITY}/identity.json")" == "null" ]] || { + echo "ERROR: ic-admin does not support encrypted pem files." + exit 1 + } >&2 + AUTH_ARGS=(--secret-key-pem "$PEM_PATH") +else + { + echo "ERROR: Please specify the HSM key ID (usually 01) or use a dfx identity with an unencrypted pem file." + exit 1 + } >&2 +fi + +# Gets the chain-fusion-signer canister ID +CANISTER_ID="$(dfx canister id signer --network "$DFX_NETWORK")" +# ... just checking... +if [[ "${DFX_NETWORK:-}" =~ mainnet|ic ]]; then + [[ "${CANISTER_ID:-}" == "grghe-syaaa-aaaar-qabyq-cai" ]] || { + echo "ERROR: Has the mainnet signer canister ID really changed?" + echo " If so, please update this script: '${BASH_SOURCE[0]}'" + exit 1 + } >&2 +fi + +# Gets the NNS URL +[[ "${DFX_NNS_URL:-}" != "" ]] || { + if [[ "${DFX_NETWORK:-}" =~ mainnet|ic ]]; then + DFX_NNS_URL="https://ic0.app" + else + # Try to use the snsdemo script for getting the provider for testnets and local replicas. + DFX_NNS_URL="$(dfx-network-provider --network "$DFX_NETWORK")" + fi +} +[[ "${DFX_NNS_URL:-}" != "" ]] || { + echo "ERROR: Unable to determine the NNS URL." + echo " Please specify --nns_url as a command line argument." + exit 1 +} >&2 + +# Gets the wasm +# Note: If the wasm is not provided, the SHA will fail. +# Note: Also, the template creation script will fail if the wasm is not provided. +WASM="release/ci/signer.wasm.gz" +WASM_SHA="$(sha256sum <"$WASM" | awk '{print $1}')" + +# Gets the canister arguments +ARG_PATH=release/ci/signer.args.bin +test -e "$ARG_PATH" || { + echo "ERROR: Arguments need to be provided in $ARG_PATH" + exit 1 +} >&2 + +# Gets the proposal text +SUMMARY_FILE=release/PROPOSAL.md +test -e "${SUMMARY_FILE:-}" || { + echo "ERROR: Proposal text not found at: ${SUMMARY_FILE}" + exit 1 +} >&2 + +# Prepares the command +set ic-admin "${AUTH_ARGS[@]}" --nns-url "$DFX_NNS_URL" propose-to-change-nns-canister --proposer "$DFX_NEURON_ID" --canister-id "$CANISTER_ID" --mode upgrade --wasm-module-path "$WASM" --summary-file "$SUMMARY_FILE" --wasm-module-sha256 "$WASM_SHA" --arg "$ARG_PATH" +# ... just checking... +echo +echo PLEASE REVIEW THIS COMMAND: +echo +echo "${@}" +echo + +read -rp "Execute? (y/N) " COMMAND_OK +if [[ "$COMMAND_OK" = [yY] ]]; then + if [[ "${PROPOSAL_ID_FILE:-}" ]]; then + # The ic-admin command writes 1 line (among others) to stdout with the following format: + # proposal proposal + # We want to parse the proposal ID out of that. First we tee stdout to + # /dev/tty so the user can also see the output, then we use grep and sed to + # extract the proposal ID and write it to a file. + "${@}" | + tee /dev/tty | + (grep -E 'proposal [0-9]+' || true) | + head -1 | + sed -E 's/.*proposal ([0-9]+)/\1/' >"$PROPOSAL_ID_FILE" + else + "${@}" + fi +fi