Upgrading a Contract
This tutorial examines how to upgrade an existing contract, a process similar to upgrading any other software. You can change an unlocked contract package by adding a new contract and updating the default contract version that the contract package should use. You will need to know the contract package hash and use the add_contract_version API. You can also create a locked contract package that cannot be versioned and is therefore not upgradable.
Video Tutorial
Here is a video walkthrough of this tutorial.
Prerequisites
- The ContractPackageHash referencing the ContractPackage where an unlocked contract is stored in global state.
- You should be familiar with writing smart contracts, on-chain contracts, and calling contracts on a Casper network.
- You have installed A Counter on the Testnet that you will upgrade as part of this tutorial.
Installing the first version of the contract (contract-v1.wasm) as shown in the counter tutorial is a prerequisite before installing the second version of the contract (contract-v2.wasm).
If you explore the code, you will observe the different versions of the contract:
contract-v1
is the counter contract you can see in the A Counter on the Testnet tutorial.contract-v2
is the contract with the newcounter_decrement
entry point.
Contract Versioning Flow
The following is an example workflow for creating a versioned contract package. Your workflow may differ if you have already created the contract package and have a handle on its hash.
- Create a contract in the most common way, using new_contract.
- Add a new version of the contract using add_contract_version.
- Build the new contract and generate the corresponding
.wasm
file. - Install the contract on the network via a deploy.
- Verify that your new contract version works as desired.
In this tutorial, you will use the second version of the counter contract to perform the upgrade.
Step 1. Create a new unlocked contract
Create a new contract using the new_contract function and store the ContractHash returned under a key in global state for later access. Under the hood, the execution engine will create a contract package (a container for the contract) that can be versioned.
When creating the contract, you can specify the package name and access URef for further modifications. Without the access key URef, you cannot add new contract versions for security reasons. Optionally, you can also save the latest version of the contract package under a named key.
// Create a new contract and specify a package name and access URef for further modifications
let (stored_contract_hash, contract_version) = storage::new_contract(
contract_entry_points,
Some(contract_named_keys),
Some("contract_package_name".to_string()),
Some("contract_access_uref".to_string()),
);
// The hash of the installed contract will be reachable through a named key
runtime::put_key(CONTRACT_KEY, stored_contract_hash.into());
// The current version of the contract will be reachable through a named key
let version_uref = storage::new_uref(contract_version);
runtime::put_key(CONTRACT_VERSION_KEY, version_uref.into());
The first version of the counter shows you a contract package that can be versioned. This step is covered in the tutorial for A Counter on the Testnet.
Additional details:
- We are versioning the contract package, not the contract. The contract is always at a set version, and the package specifies the contract version to be used.
- The Wasm file name of the new contract could differ from the old contract; after sending the deploy to the network, the contract package hash connects the different contract versions.
Step 2. Add a new contract to the package
There are many changes you could make to a Casper contract, like adding new entry points, modifying the behavior of an existing entry point, or completely re-writing the contract.
To add a new contract version in the package, invoke the add_contract_version function and pass in the ContractPackageHash, EntryPoints, and NamedKeys. In the counter example, you will find the add_contract_version
call here.
let (contract_hash, contract_version) =
storage::add_contract_version(contract_package_hash,
entry_points,
named_keys);
Explanation of arguments:
contract_package_hash
- This hash directs you to the contract package. See Hash and Key Explanations.entry_points
- Entry points of the contract, which can be modified or newly added.named_keys
- Named key pairs of the contract.
The new contract version carries on named keys from the previous version. If you specify a new set of named keys, they will be combined with the old named keys in the new contract version. If the old and new contract versions use the same named keys, then the new values would be present in the new version of the contract.
You will need to manage contract versioning, considering clients that may use older versions. Here are a few options:
- Pin your client contract to the contract hash of a specific version.
- Use call_versioned_contract with a version number to pin your client contract to that version.
- Call a contract using call_versioned_contract and version "None", which uses the newest version of the contract.
Step 3. Build the contract Wasm
Use these commands to prepare and build the newly added contract:
make prepare
make build-contract
Step 4. Install the contract
Install the contract on the network via a deploy and verify the deploy status. You can also monitor the event stream to see when your deploy is accepted.
To observe the upgrade workflow, you can install the second contract version on the chain. This version contains the counter_decrement
entry point.
Installing the first version of the contract, as shown in the Counter tutorial, is a prerequisite before installing the second version.
casper-client put-deploy \
--node-address http://[NODE_IP]:7777 \
--chain-name [CHAIN_NAME] \
--secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \
--payment-amount [PAYMENT_AMOUNT_IN_MOTES] \
--session-path [PATH]/contract-v2/target/wasm32-unknown-unknown/release/counter-v2.wasm
Step 5. Verify your changes
You can write unit tests to verify the behavior of the new contract version with call_contract or call_versioned_contract. When you add a new contract to the package (which increments the highest enabled version), you will obtain a new contract hash, the primary identifier of the contract. You can use the contract hash with call_contract. Alternatively, you can use call_versioned_contract and specify the contract_package_hash and the newly added version.
For the simple example counter above, here are the corresponding tests. Notice how the tests store and verify the contract's version and entry points.
You could store the latest version of the contract package under a NamedKey, as shown here. Then, you can query the NamedKey to check the latest version of the contract package.
Example test function
// Verify the contract version is now 2.
let account = builder
.get_account(*DEFAULT_ACCOUNT_ADDR)
.expect("should have account");
let version_key = *account
.named_keys()
.get(CONTRACT_VERSION_KEY)
.expect("version uref should exist");
let version = builder
.query(None, version_key, &[])
.expect("should be stored value.")
.as_cl_value()
.expect("should be cl value.")
.clone()
.into_t::<u32>()
.expect("should be u32.");
assert_eq!(version, 2);
You can also test the new entry point by using the Rust command-line client.
Get the NEW state-root-hash:
casper-client get-state-root-hash --node-address http://[NODE_IP]:7777
Check the new contract entry points. You should see the counter_decrement entry point now.
casper-client query-global-state \
--node-address http://[NODE_IP]:7777 \
--state-root-hash [STATE_ROOT_HASH] \
--key [ACCOUNT_HASH] -q "counter"
Example output
{
"id": 5602352547578277096,
"jsonrpc": "2.0",
"result": {
"api_version": "1.4.13",
"block_header": null,
"merkle_proof": "[54054 hex chars]",
"stored_value": {
"Contract": {
"contract_package_hash": "contract-package-wasmc014187ccf3366cca70317d6d567cd56a05ecf1ee50ed3bd02727c2864e3d3a8",
"contract_wasm_hash": "contract-wasm-64d252f1ab72c7295a85d15c3f456f8bdda586580b0b7106e203fa4fd83f05d7",
"entry_points": [
{
"access": "Public",
"args": [],
"entry_point_type": "Contract",
"name": "counter_decrement",
"ret": "Unit"
},
{
"access": "Public",
"args": [],
"entry_point_type": "Contract",
"name": "counter_get",
"ret": "I32"
},
{
"access": "Public",
"args": [],
"entry_point_type": "Contract",
"name": "counter_inc",
"ret": "Unit"
}
],
"named_keys": [
{
"key": "uref-ca980a2e4c08dc3f233b728b22b909cd4e894295155a7902bf88a59eac1531d1-007",
"name": "count"
}
],
"protocol_version": "1.4.13"
}
}
}
}
Check the version and package details with the latest state root hash:
casper-client query-global-state \
--node-address http://[NODE_IP]:7777 \
--state-root-hash [STATE_ROOT_HASH] \
--key [ACCOUNT_HASH] -q "version"
Example output
{
"id": 9084525900533244372,
"jsonrpc": "2.0",
"result": {
"api_version": "1.4.13",
"block_header": null,
"merkle_proof": "[64874 hex chars]",
"stored_value": {
"CLValue": {
"bytes": "02000000",
"cl_type": "U32",
"parsed": 2
}
}
}
casper-client query-global-state \
--node-address http://[NODE_IP]:7777 \
--state-root-hash [STATE_ROOT_HASH] \
--key [ACCOUNT_HASH] -q "counter_package_name"
Example output
{
"id": 6933525663267881367,
"jsonrpc": "2.0",
"result": {
"api_version": "1.4.13",
"block_header": null,
"merkle_proof": "[52174 hex chars]",
"stored_value": {
"ContractPackage": {
"access_key": "uref-101817ffd5aa47b08ca710649dbdc41edf0a20d7802c736d34053656c0a99eaf-007",
"disabled_versions": [],
"groups": [],
"versions": [
{
"contract_hash": "contract-4ee8a4cfbc0a183d189611b6a14c0f7b57e7635fa17a8acfc5c645fec4c36316",
"contract_version": 1,
"protocol_version_major": 1
},
{
"contract_hash": "contract-2cd9f6485423ba846fae83729016b03df26d9babb939466906c8f1d168b40949",
"contract_version": 2,
"protocol_version_major": 1
}
]
}
}
}
}
Call the new entry point, counter_decrement, using the package name and check the results.
casper-client put-deploy \
--node-address http://[NODE_IP]:7777 \
--chain-name [CHAIN_NAME] \
--secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \
--payment-amount [PAYMENT_AMOUNT_IN_MOTES] \
--session-package-name "counter_package_name" \
--session-entry-point "counter_decrement"
There are two ways to call versioned contracts:
After calling the entry point, the count value should be decremented. You can verify it by querying the network again using the new state root hash.
Disabling and Enabling Contract Versions
You can disable a contract version within a contract package by using the disable_contract_version function.
Disabled contract versions can no longer be executed. As such, if there is only a single contract version within the package, you will no longer be able to use the contract.
Enable_contract_version allows you to re-enable a previously disabled contract version.
::note
Be aware that calling a contract package will use the most recent contract version. It is not necessary to disable a previous contract version, unless you have a specific need to do so.
:::
Creating a Locked Contract Package
You can create a locked contract package with the new_locked_contract function. This contract can never be upgraded.
let (stored_contract_hash, _) = storage::new_locked_contract(
contract_entry_points,
Some(contract_named_keys),
Some("contract_package_name".to_string()),
Some("contract_access_uref".to_string()),
);
Apply the contract entry points and named keys when you call the function. You can also specify a hash_name and uref_name that will be put in the context's named keys. You do not need to save the version number returned since the version of this contract package would always be equal to 1.
Creating a locked contract package is an irreversible decision. To upgrade a contract, use new_contract as Step 1 explains.