Skip to main content

Testing on mainnet (SPS testflight)

Advanced
Governance
Tutorial

Overview

Once a developer has tested the process of an SPS, it is highly recommended they do an "SPS testflight" on the mainnet. An SPS testflight is when a developer deploys their dapp (to the mainnet) and hands control of it to a mock SPS (on the mainnet).

The main intent of performing an SPS testflight is for a developer to experience how a dapp works after it has been decentralized, so developers can make sure their dapp is ready for decentralization. It does not test the actual process of decentralizing it.

A testflight is not a repo or set of tools, but an activity (deploying a dapp and handing control of it to a mock SPS), so the instructions for testing on the mainnet utilize various tools, but developers can of course use any tools they wish.

Among other things, here are some examples of issues developers find after running an SPS testflight:

  • Developers notice they need a better pipeline for creating proposals to update a dapp.
  • Developers notice they may have been decentralized prematurely and need to fix some things.
  • Developers notice they may need better monitoring before decentralizing.

The mock SPS used in an SPS testflight gives developers the ability to see what the post-decentralization lifecycle of a dapp looks like, but still provides a way for a developer to keep control of their dapp. Therefore, developers are encouraged to perform an SPS testflight on the mainnet, potentially for multiple days or weeks, to ensure that all aspects have been covered.

SPS testflight vs. SPS production

The main differences to a production SPS deployment are summarized here:

  • Testflight SPS is deployed by the developer instead of the NNS; in particular, no NNS proposals are involved.
  • No decentralization swap is actually performed; in particular, the developer has full control over the SPS for the entire duration of the testflight.
  • The developer can also keep direct control over the dapp's canisters registered with testflight SPS.
  • When deployed on the mainnet, testflight SPS is deployed to a regular application subnet instead of a dedicated SPS subnet.

Prerequisites

To perform an SPS testflight using the instructions that follow, you will need the following tools:

Developers can use any set of tools that accomplish the goals of a testflight.

Installing dfx's sns extension

The extension can be installed from dfx itself:

dfx extension install sns --version 0.4.0

Versions

This guide has been tested with the following version of the tools:

  • dfx: 0.16.1
  • dfx's sns extension: 0.4.0
  • quill: v0.4.2
  • didc: 0.3.0 (2022-11-17)

High-level

A testflight typically consists of the following steps:

  1. Import and download the SPS canisters.
  2. Deploy testflight SPS (mock SPS canisters to an application subnet) on the mainnet and store the developer neuron ID.
  3. Deploy a dapp.
  4. Register a dapp (hand over control) to the mock SPS canister.
  5. Test upgrading canisters via SPS proposals.

Activities developers may need to do in running a testflight:

  • Check the proposals.
  • Test executing code on SPS managed canisters via SPS proposals.
  • Abort the testflight.

Steps to run a testflight

If you would like to deploy the testflight locally before trying the testflight on mainnet (which requires cycles), first start a local replica:

dfx start --clean

Step 1. Import and download SPS canisters

To import the SPS canisters in the dfx.json file of your project and download their WASM binaries, run

DFX_IC_COMMIT=94bbea43c7585a1ef970bd569a447c269af9650b dfx sns import
DFX_IC_COMMIT=94bbea43c7585a1ef970bd569a447c269af9650b dfx sns download

in the root directory of your project.

Step 2. Deploy testflight SPS (mock SPS canisters to an application subnet) on mainnet and store the developer neuron ID

To deploy the testflight SPS, run

dfx sns deploy-testflight --init-config-file="/path/to/sns_init.yaml"

To deploy on the mainnet, pass --network ic as an additional argument.

After the deployment finishes, make sure to store the developer neuron ID which you will use to submit SPS proposals. The testflight SPS parameters are configured so that this developer neuron ID has full control over the testflight SPS. The developer neuron ID is the last part of the testflight deployment output, e.g.:

Developer neuron IDs:

Step 3. Add SPS root as an additional controller of all SPS managed dapp canisters

Add the SPS root canister as an additional controller of all the canisters that you want to manage by the testflight SPS. For a canister called test, you can do so as follows:

dfx canister update-settings --add-controller $(dfx canister id sns_root) test

When running the testflight on the mainnet, pass --network ic as an additional argument to both invocations of dfx canister.

Step 4. Register dapp canisters with SPS root

Register all canisters that are supposed to be managed by the testflight SPS by submitting an SPS proposal via quill.

When running the testflight locally, export the environment variable IC_URL to point to your local replica instance, e.g.,

export IC_URL="http://localhost:8080/"

Determine the absolute path to the PEM file of your identity. Typically, this file is located at .config/dfx/identity/$(dfx identity whoami)/identity.pem under your home directory.

Finally, prepare and send the SPS proposal via quill:

export DEVELOPER_NEURON_ID="594fd5d8dce3e793c3e421e1b87d55247627f8a63473047671f7f5ccc48eda63"
export PEM_FILE="/home/martin/.config/dfx/identity/$(dfx identity whoami)/identity.pem"
export CID="$(dfx canister id test)"
quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal --proposal "(record { title=\"Register dapp's canisters with SPS.\"; url=\"https://example.com/\"; summary=\"This proposal registers dapp's canisters with SPS.\"; action=opt variant {RegisterDappCanisters = record {canister_ids=vec {principal\"$CID\"}}}})" $DEVELOPER_NEURON_ID > register.json
quill send register.json

When running the testflight on the mainnet, pass --network ic as an additional argument to dfx canister when obtaining the dapp's canister IDs. Otherwise, pass --insecure-local-dev-mode as an additional argument to quill send.

You can also register multiple canisters via a single SPS proposals by adjusting RegisterDappCanisters in the --proposal argument above to, e.g.,

RegisterDappCanisters = record {canister_ids=vec {principal\"$CID1\"; principal\"$CID2\";}}

Once the SPS proposal is executed, you should see all the registered dapp's canisters listed as dapps in

dfx canister call sns_root list_sns_canisters '(record {})'

When running the testflight on the mainnet, pass --network ic as an additional argument to dfx canister above.

Step 5. Test upgrading canisters via SPS proposals

Determine the path to the new wasm binary that you want to upgrade the canister to. For projects built with dfx, this binary is typically located at .dfx/<network>/canisters/<canister-name>/<canister-name>.wasm under the root directory of your project, where <network> is the network (e.g., local or ic) and <canister-name> is the name of the canister according to dfx.json.

Now you can prepare and send the SPS proposal via quill:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-upgrade-canister-proposal --summary "This proposal upgrades test canister." --title "Upgrade test canister." --url "https://example.com/" --target-canister-id $CID --wasm-path "./.dfx/local/canisters/test/test.wasm" $DEVELOPER_NEURON_ID > upgrade.json

quill send upgrade.json | grep -v "^ *new_canister_wasm"

Unless you run the testflight against the mainnet, pass --insecure-local-dev-mode as an additional argument to quill send.

You can omit grep -v "^ *new_canister_wasm" above to see the new WASM binary in the output. Note that the output then contains the entire WASM binary and can be huge!

Activities developer may need to do

Test executing code on SPS managed canisters via SPS proposals

To execute code on SPS managed canisters via SPS proposals, you can use generic proposals. To use such proposals, the SPS managed canisters that define the behavior of such a proposal must expose a pair of public functions (referred to as generic functions in the following):

  • a validation function to validate and render the proposal payload;
  • an execution function to perform an action given the proposal payload.

The validation function must return a value of the Candid type variant { Ok: text; Err: text; }, e.g., Result<String, String> in Rust. If the validation function returns Ok(rendering), then the proposal is submitted and the rendering string is included into the proposal. Otherwise, the proposal submission fails.

The execution function gets the same binary payload as passed to the validation function and its code gets executed if the proposal is accepted. It should not return any value because this return value is ignored.

To use generic functions, you must first submit an SPS proposal to register these functions with SPS governance:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal --proposal "(record { title=\"Register generic functions.\"; url=\"https://example.com/\"; summary=\"This proposals registers generic functions.\"; action=opt variant {AddGenericNervousSystemFunction = record {id=1000:nat64; name=\"MyGenericFunctions\"; description=null; function_type=opt variant {GenericNervousSystemFunction=record{validator_canister_id=opt principal\"$CID\"; target_canister_id=opt principal\"$CID\"; validator_method_name=opt\"validate\"; target_method_name=opt\"execute\"}}}}})" $DEVELOPER_NEURON_ID > register-generic-functions.json

quill send register-generic-functions.json

You have to assign a distinct numeric identifier to all generic functions registered with SPS governance. Note that this identifier has to be at least 1000. You also have to provide a name and an optional description of the generic functions that are rendered in the proposal, but do not directly relate to the functions' names in the canister code (the name is MyGenericFunctions and the description is omitted in the sample code above).

Once the proposal to register generic functions is accepted, you can submit proposals to execute them with a given binary payload:

quill sns --canister-ids-file ./sns_canister_ids.json --pem-file $PEM_FILE make-proposal --proposal "(record { title=\"Execute generic functions.\"; url=\"https://example.com/\"; summary=\"This proposal executes generic functions.\"; action=opt variant {ExecuteGenericNervousSystemFunction = record {function_id=1000:nat64; payload=blob \"DIDL\01l\02\b9\fa\ee\18y\b5\f6\a1Cy\01\00\02\00\00\00\03\00\00\00\"}}})" $DEVELOPER_NEURON_ID > execute-generic-functions.json

quill send execute-generic-functions.json

The generic functions to be executed are specified by their numeric identifier defined in their registration proposal. The payload is a blob that is passed to both generic functions. The above sample payload blob \"DIDL\01l\02\b9\fa\ee\18y\b5\f6\a1Cy\01\00\02\00\00\00\03\00\00\00\" was obtained via

didc encode '(record {major=2:nat32; minor=3:nat32;})' --format blob
blob "DIDL\01l\02\b9\fa\ee\18y\b5\f6\a1Cy\01\00\02\00\00\00\03\00\00\00"

and can be decoded as Candid payload (a record with two fields) in the canister Rust code:

#[derive(CandidType, Debug, Deserialize)]
struct Version {
major: u32,
minor: u32,
}

#[ic_cdk::update]
fn validate(x: Version) -> Result<String, String> {
// ...
}

Check the proposals of the testflight SPS

You can list all proposals in the testflight SPS as follows:

dfx canister call sns_governance list_proposals '(record {include_reward_status = vec {}; limit = 0; exclude_type = vec {}; include_status = vec {};})'

When running the testflight on the mainnet, pass --network ic as an additional argument to dfx canister. You can also provide a limit and thus only obtain the last few proposals.

Aborting the testflight

As the developer keeps direct control over the registered dapp's canisters during the testflight, you can directly manage your dapp's canisters during the testflight if needed. However, you are strongly encouraged to make sure you can also perform all required operations only via SPS proposals (possibly after upgrading your dapp).

Once you are done with testing all kinds of SPS proposals needed to operate your dapp, you can finish the testflight. Make sure that you are a controller of all the canisters registered with the testflight SPS, e.g., by invoking

dfx canister status test

When running the testflight on the mainnet, pass --network ic as an additional argument to dfx canister.

If this is the case, you can safely delete the SPS testflight canisters. Otherwise, you can restore control over your dapp's canisters by reinstalling the SPS root canister with the following code (make sure to paste the developer principal ID and your dapp's canister ID into the code):

use candid::{CandidType, Principal};

#[derive(Default, Clone, CandidType, Debug)]
pub struct CanisterSettingsArgs {
pub controllers: Option<Vec<candid::Principal>>,
pub compute_allocation: Option<candid::Nat>,
pub memory_allocation: Option<candid::Nat>,
pub freezing_threshold: Option<candid::Nat>,
}

#[derive(CandidType)]
pub struct UpdateSettingsArgs {
pub canister_id: candid::Principal,
pub settings: CanisterSettingsArgs,
pub sender_canister_version: Option<u64>,
}

#[ic_cdk::update]
async fn recover() {
// put your developer principal here:
let developer_principal = Principal::from_text("").unwrap();
let can_settings = CanisterSettingsArgs {
controllers: Some(vec![developer_principal]),
compute_allocation: None,
memory_allocation: None,
freezing_threshold: None,
};
// put the registered canister ID here:
let canister_id = Principal::from_text("").unwrap();
let settings = UpdateSettingsArgs {
canister_id,
settings: can_settings,
sender_canister_version: None,
};
ic_cdk::api::call::call::<_, ()>(Principal::from_text("aaaaa-aa").unwrap(), "update_settings", (settings,)).await.unwrap();
}

and invoking:

dfx canister call sns_root recover '()'

When running the testflight on the mainnet, pass --network ic as an additional argument to dfx canister.