Updates to Cadence 1.0 Contract Staging

Overview

This post relates to the changes to the staging contract outlined in FLIP 179. While the mechanism outlined in that FLIP may be useful for the general purpose of coordinating contract updates, it was decided that the network-wide updates required for Cadence 1.0 would be better served by state migration.

To ease the path for both state migration as well as developers updating their contracts, the staging contract has been simplified for the purposes of storing staged code, broadcasting when changes are made to staged code (addition, replacement, and removal), and serving queries to retrieve said code.

What follows is a primer on the updated staging mechanic with the hopes of providing clear primitives on which enhanced developer tooling can be built.

Staging Process

Pre-Requisites

  • An existing contract deployed to your target network. For example, if you’re staging A in address 0x01, you should already have a contract named A deployed to 0x01.
  • A Cadence 1.0 compatible contract serving as an update to your existing contract. Extending our example, if you’re staging A in address 0x01, you should have a contract named A that is Cadence 1.0 compatible. See the references below for more information on Cadence 1.0 language changes.

Staging Your Contract Update

Armed with your pre-requisites, you’re ready to stage your contract update. Run the stage_contract.cdc transaction, passing your contract’s name and Cadence code as arguments and signing as the contract host account.

import "MigrationContractStaging"

transaction(contractName: String, contractCode: String) {
    let host: &MigrationContractStaging.Host
    
    prepare(signer: AuthAccount) {
        // Configure Host resource if needed
        if signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath) == nil {
            signer.save(<-MigrationContractStaging.createHost(), to: MigrationContractStaging.HostStoragePath)
        }
        // Assign Host reference
        self.host = signer.borrow<&MigrationContractStaging.Host>(from: MigrationContractStaging.HostStoragePath)!
    }

    execute {
        // Call staging contract, storing the contract code that will update during Cadence 1.0 migration
        MigrationContractStaging.stageContract(host: self.host, name: contractName, code: contractCode)
    }

    post {
        MigrationContractStaging.isStaged(address: self.host.address(), name: contractName):
            "Problem while staging update"
    }
}

At the end of this transaction, your contract will be staged in the MigrationContractStaging account. If you staged this contract’s code previously, it will be overwritten by the code provided in this transaction.

Checking Staging Status

Retrieving staged code will be critical for both developers building on unowned dependencies as well as the creation of state migrations based on staged code. To do so, run the get_staged_contract_code.cdc script, passing the address & name of the contract you’re requesting and getting the Cadence code in return. This script can also help you get the staged code for your dependencies if the project owner has staged their code.

import "MigrationContractStaging"

/// Returns the code as it is staged or nil if it not currently staged.
///
access(all) fun main(contractAddress: Address, contractName: String): String? {
    return MigrationContractStaging.getStagedContractCode(address: contractAddress, name: contractName)
}

Developer Support

While the MigrationContractStaging contract serves as a central repository for staged code, much more can be done to support developers during the staging process as well as ensure a rapid feedback loop regarding status of both their staged updates and any unowned dependency contracts.

It will be critical for network health that the refactor and staging process be as simple and frictionless as possible. Below are several crowdsourced tooling ideas for that might make for good next steps given the interface and primitives provided by this contract:

  • Adding a staged contract badge on contracts in popular web tools such as Flowdiver, ContractBrowser, Flowview, etc.
  • Diff views comparing current & staged contracts in contract inspectors including Flowdiver, ContractBrowser, etc.
  • A GUI from which developers can connect & stage their contracts
  • A dashboard to track network-wide staging progress
  • Flow CLI command that stages a contract, replacing contract aliases for the named network and running a sanity check (such as a 1.0 parser) on the code before submitting the staging transaction
  • A page where one can create a watchlist of contracts to check staging status and/or be notified when they’re staged
  • Emulator + CLI version that enables mocking the HCU locally so you can test staging & migration of your staged contracts locally

These ideas are not an exhaustive list and are presented in no particular order, so please do provide feedback. In the interest of fostering ideation towards this effort, below is a summary of the contract interface.

MigrationContractStaging Contract Details

The basic interface to stage a contract is the same as deploying a contract - name + code. See the stage_contract & unstage_contract transactions. Note that calling stageContract() again for the same contract will overwrite any existing staged code for that contract.

/// 1 - Create a host and save it in your contract-hosting account at MigrationContractStaging.HostStoragePath
access(all) fun createHost(): @Host
/// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage.
access(all) fun stageContract(host: &Host, name: String, code: String)
/// Removes the staged contract code from the staging environment.
access(all) fun unstageContract(host: &Host, name: String)

To stage a contract, the developer first saves a Host resource in their account which they pass as a reference along with the contract name and code they wish to stage. The Host reference simply serves as proof of authority that the caller has access to the contract-hosting account, which in the simplest case would be the signer of the staging transaction, though conceivably this could be delegated to some other account via Capability - possibly helpful for some multisig contract hosts.

/// Serves as identification for a caller's address.
access(all) resource Host {
    /// Returns the resource owner's address
    access(all) view fun address(): Address
}

Within the MigrationContractStaging contract account, code is saved on a contract-basis as a ContractUpdate struct within a Capsule resource and stored at a the derived path. The Capsule simply serves as a dedicated repository for staged contract code.

/// Represents contract and its corresponding code.
access(all) struct ContractUpdate {
    access(all) let address: Address
    access(all) let name: String
    access(all) var code: String

    /// Validates that the named contract exists at the target address.
    access(all) view fun isValid(): Bool
    /// Serializes the address and name into a string of the form 0xADDRESS.NAME
    access(all) view fun toString(): String
    /// Replaces the ContractUpdate code with that provided.
    access(contract) fun replaceCode(_ code: String)
}

/// Resource that stores pending contract updates in a ContractUpdate struct.
/// Saved at /storage/MigrationContractStaging.capsulePathPrefix_ADDRESS_NAME
access(all) resource Capsule {
    /// The address, name and code of the contract that will be updated.
    access(self) let update: ContractUpdate

    /// Returns the staged contract update in the form of a ContractUpdate struct.
    access(all) view fun getContractUpdate(): ContractUpdate
    /// Replaces the staged contract code with the given Cadence code.
    access(contract) fun replaceCode(code: String)
}

To support monitoring staging progress across the network, the single StagingStatusUpdated event is emitted any time a contract is staged (status == "stage"), staged code is replaced (status == "replace"), or a contract is unstaged (status == "unstage").

/// Event emitted when a contract's code is staged, replaced or unstaged
/// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract
/// NOTE: Does not guarantee that the contract code is valid Cadence
access(all) event StagingStatusUpdated(
  capsuleUUID: UInt64,
  address: Address,
  code: String,
  contract: String,
  action: String
)

Included in the contact are methods for querying staging status and retrieval of staged code. This enables platforms like Flowview, Flowdiver, ContractBrowser, etc. to display the staging status of contracts on any given account.

/* --- Public Getters --- */
//
/// Returns true if the contract is currently staged.
access(all) view fun isStaged(address: Address, name: String): Bool
/// Returns the names of all staged contracts for the given address.
access(all) view fun getStagedContractNames(forAddress: Address): [String]
/// Returns the staged contract Cadence code for the given address and name.
access(all) view fun getStagedContractCode(address: Address, name: String): String?
/// Returns an array of all staged contract host addresses.
access(all) view fun getAllStagedContractHosts(): [Address]
/// Returns a dictionary of all staged contract code for the given address.
access(all) view fun getAllStagedContractCode(forAddress: Address): {String: String}
/// Returns all staged contracts as a mapping of address to an array of contract names
access(all) view fun getAllStagedContracts(): {Address: [String]}

Next Steps

Assuming alignment following on from conversation in the recent PR, the staging contract will be deployed to a Flow-managed Testnet account by the end of this week. Once deployed, tooling can be built against it to smoothen the staging path. Please feel free to ask any questions or highlight any gaps in the staging process outlined above.

4 Likes

Great work! :clap:

2 Likes

Will flow-cli be updated with some new commands to help with the contract stage?

1 Like

Yes, I believe staging integration with Flow CLI is planned → Add Staging commands to CLI · Issue #1375 · onflow/flow-cli · GitHub

2 Likes

Hi @gio_on_flow

Quick question regarding Cadence-1.0 contracts upgrade:

If we stage a upgraded contract but the code breaks the rules here (https://cadence-lang.org/docs/1.0/language/contract-updatability#structs-resources-and-interfaces), what would happen when Crescendo network upgrades?

Asking this as C1.0 is not backward-compatible and there’re way many conditions that the upgraded code would violate the contract-upgrade rules. For example we have something like this in C0.42:

pub contract interface OracleInterface {
    pub resource PriceReader {
        ...
    }
}

Then we need to upgrade it to:

access(all) contract interface OracleInterface {
    access(all) resource interface PriceReader {
        ...
    }
}

As resource declarations cannot be nested inside contract interface declarations in C1.0. But it violates the contract-upgrade rules as it is removing an existing declaration.

What’s your take on this, what shall we do then? Or is there an one-time exception for contracts to be upgraded in the Crescendo network upgrade?

1 Like

We’re currently discussing and implementing the modified rules for the contract update checking that are necessary for developers to update to Cadence 1.0, and will document them once we have verified them.

Changing nested type requirements to interfaces is exactly one of those changes that are normally not allowed, but will be allowed in the Cadence 1.0 upgrade.

4 Likes

Thank u bastian!

2 Likes