From fbeeec014ab5eb50deeea41611d3dc0ea02d3165 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 1 Jun 2023 00:10:22 -0700 Subject: [PATCH 01/10] Adding a resource accounts tutorial that explains the different ways to use a resource account. Added an example in move-examples and a unit test for it for publishing an upgradeable module. --- aptos-move/e2e-move-tests/src/tests/mod.rs | 1 + .../src/tests/upgrade_resource_contract.rs | 66 +++++ .../upgrade_resource_contract/Move.toml | 7 + .../sources/upgrader.move | 35 +++ .../docs/concepts/resource-accounts.md | 259 ++++++++++++++++++ developer-docs-site/sidebars.js | 1 + 6 files changed, 369 insertions(+) create mode 100644 aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs create mode 100644 aptos-move/move-examples/upgrade_resource_contract/Move.toml create mode 100644 aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move create mode 100644 developer-docs-site/docs/concepts/resource-accounts.md diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index 4b3635f089fb4..6ae31d6b150b5 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -39,5 +39,6 @@ mod token_event_store; mod token_objects; mod transaction_fee; mod type_too_large; +mod upgrade_resource_contract; mod vector_numeric_address; mod vote; diff --git a/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs b/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs new file mode 100644 index 0000000000000..e2812ad77652a --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs @@ -0,0 +1,66 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::{assert_success, tests::common, MoveHarness}; +use aptos_types::{ + account_address::{create_resource_address, AccountAddress}, +}; + +// Note: this module uses parameterized tests via the +// [`rstest` crate](https://crates.io/crates/rstest) +// to test for multiple feature combinations. + +#[test] +fn code_upgrading_using_resource_account() { + let mut h = MoveHarness::new(); + + let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + let resource_address = create_resource_address(*acc.address(), &[]); + + // add the named addresses for `owner` and `upgrade_resource_contract` + let mut build_options = aptos_framework::BuildOptions::default(); + build_options + .named_addresses + .insert("owner".to_string(), *acc.address()); + build_options + .named_addresses + .insert("upgrade_resource_contract".to_string(), resource_address); + + // build the package from our example code + let package = aptos_framework::BuiltPackage::build( + common::test_dir_path("../../../move-examples/upgrade_resource_contract"), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_code(); + let metadata = package + .extract_metadata() + .expect("extracting package metadata must succeed"); + + // create the resource account and publish the module under the resource account's address + let result = h.run_transaction_payload( + &acc, + aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account_and_publish_package( + vec![], + bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), + code.clone(), + ), + ); + + assert_success!(result); + + // test upgrading the code + assert_success!(h.run_entry_function( + &acc, + str::parse(&format!( + "0x{}::upgrader::upgrade_contract", + resource_address + )).unwrap(), + vec![], + vec![ + bcs::to_bytes(&bcs::to_bytes(&metadata).unwrap()).unwrap(), + bcs::to_bytes(&code).unwrap(), + ], + )); +} diff --git a/aptos-move/move-examples/upgrade_resource_contract/Move.toml b/aptos-move/move-examples/upgrade_resource_contract/Move.toml new file mode 100644 index 0000000000000..4a993944139d9 --- /dev/null +++ b/aptos-move/move-examples/upgrade_resource_contract/Move.toml @@ -0,0 +1,7 @@ +[package] +name = "Upgrade Resource Account Contract" +version = "0.0.0" +upgrade_policy = "compatible" + +[dependencies] +AptosFramework = { local = "../../framework/aptos-framework" } \ No newline at end of file diff --git a/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move b/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move new file mode 100644 index 0000000000000..03574402b819b --- /dev/null +++ b/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move @@ -0,0 +1,35 @@ +module upgrade_resource_contract::upgrader { + use std::signer; + use std::account::{SignerCapability}; + use std::resource_account; + use std::account; + use std::code; + + struct MySignerCapability has key { + resource_signer_cap: SignerCapability, + } + + fun init_module(resource_signer: &signer) { + assert!(signer::address_of(resource_signer) == @upgrade_resource_contract, 0); + let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @owner); + move_to(resource_signer, MySignerCapability { + resource_signer_cap: resource_signer_cap, + }); + } + + // Note the assertion that the caller is @owner. If we leave this line out, anyone can upgrade the contract, exposing the resource account's resources and the contract functionality. + public entry fun upgrade_contract( + owner: &signer, + metadata_serialized: vector, + code: vector>, + ) acquires MySignerCapability { + assert!(signer::address_of(owner) == @owner, 1); + let resource_signer_cap = &borrow_global(@upgrade_resource_contract).resource_signer_cap; + let resource_signer = account::create_signer_with_capability(resource_signer_cap); + code::publish_package_txn( + &resource_signer, + metadata_serialized, + code, + ); + } +} \ No newline at end of file diff --git a/developer-docs-site/docs/concepts/resource-accounts.md b/developer-docs-site/docs/concepts/resource-accounts.md new file mode 100644 index 0000000000000..02f231a83ccc4 --- /dev/null +++ b/developer-docs-site/docs/concepts/resource-accounts.md @@ -0,0 +1,259 @@ +--- +title: "Resource Accounts" +id: "resource-accounts" +--- + +## What is a resource account? + +A resource account is an [Account](https://aptos.dev/concepts/accounts/) that's used to store and manage resources. It can be a simple storage account that's used merely to separate different resources for an account or a module, or it can be utilized to programmatically control resource management in a contract. + +There are two distinct ways to manage a resource account. In this guide we'll discuss how to implement each technique, variations on the implementations, and any configuration details relevant to the creation process. + +## How to utilize a resource account + +### Manually controlling a resource account by rotating its auth key to another account's auth key + +The first technique we're going to discuss is through the `create_resource_account` function in the `resource_account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). + +You can specify a `seed` byte vector and an optional auth key to rotate the resulting resource account's auth key to. +```rust +public entry fun create_resource_account( + origin: &signer, + seed: vector, + optional_auth_key: vector, +) acquires Container { + let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + resource_signer_cap, + optional_auth_key, + ); +} +``` +When you create a resource account like this, the account with the matching auth key can sign for it. Transactions signed in this way will show the signer/sender account as the resource account, but it was actually signed by the owning account with the matching auth key. This is mostly for separating resources into a different account- it's merely a way to organize and manage resources. + +Notice that there is nothing returned here- we are not given anything to store or manage. We simply created a resource account and rotated its auth key to the optional auth key. + +:::tip +If you don't specify an auth key, that is, if you pass in `vector::empty()` or `vector []` to the `optional_auth_key` field, it will automatically rotate the auth key to the `origin` account's auth key. +::: + +### Managing a resource account programmatically with a SignerCapability + +The second technique is the `create_resource_account` function in the `account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). + +```rust +public fun create_resource_account( + source: &signer, + seed: vector +): (signer, SignerCapability) acquires Account { + // ... +} +``` + +When this function is called, the auth key of the resource account is rotated to `0x0`, which gives the Move VM the capability to generate the resource account's signer from a `SignerCapability`. You can store this `SignerCapability` and retrieve it later to sign for the resource account. + +This is often integral to automating a smart contract in Move. It gives the developer the ability to generate a signer for an account programmatically. + +Notice that the creation function returns the resource account's `signer` and a `SignerCapability` resource. Let's discuss what a `SignerCapability` is and then how to store it. + +#### What's a SignerCapability? + +A SignerCapability is a very simple resource, mostly meant to abstractly represent the ability to sign for an account. It doesn't actually *do* anything special, but its existence somewhere implies that if you have access to it, you either created it or received access to it very intentionally. + +It contains a single field called `account` the matches the address it's intended to generate a signature for: + +```rust +struct SignerCapability has drop, store { + account: address +} +``` + +Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. + +Here is a very basic example that demonstrates how you'd use a `SignerCapability` in a Move contract: + +```rust +// define a resource we can store the SignerCapability in. We use key here for simplicity's sake +struct MySignerCapability has key { + resource_signer_cap: SignerCapability, +} + +public entry fun store_signer_capability(creator: &signer) { + // We store `MySignerCapability` to an account's resources. We can even store it on the resource account itself: + let (resource_signer, resource_signer_cap) = account::create_resource_account(creator, b"seed bytes"); + move_to(resource_signer, MySignerCapability { + resource_signer_cap, + }); +} + +// Now we utilize the resource account by generating its signer with the SignerCapability +public entry fun sign_with_resource_account(creator: &signer) acquires MySignerCapability { + let resource_address = account::create_resource_address(signer::address_of(creator), b"seed bytes"); + let signer_cap = borrow_global(resource_account_address); + let resource_signer = account::create_signer_with_capability(signer_cap); + + // here we'd do something with the resource_signer that we can only do with its `signer`, like transfer coins, create/transfer an NFT, or call some other function that rqeuires a signer. + // be careful with making functions like these entry functions. If you have no contingencies for a function like this, they can be very easily abused. +} +``` +Utilizing a resource account in this way is the fundamental process for automating the generation and retrieval of resources on-chain. + +You might be wondering "*Why does this work? Isn't it dangerous to be able to create a signer for an account so easily?*" + +Yes, you need to make sure you're gating access to a `SignerCapability` whenever you store it somewhere. Be very thoughtful with how you facilitate access to one, because unrestricted access to it gives free reign for anyone to call any function that requires a signer with it. + +:::tip +To intuitively understand why a `SignerCapability` is allowed to be so powerful, you need to consider how resource storage and control work in Move. You can't directly access, create, or modify a resource outside of the module it's defined in, meaning if you have access to a resource in some way, the creator of the module it belongs to explicitly gave it to you. + +Upon creating the `SignerCapability`, you're free to decide how you want to expose it. You can store it somewhere, give it away, or gate its access to functions that use it or conditionally return it. +::: + +### Using a resource account to publish a module + +There are a few other ways we can utilize a resource account. One common usage is to use it to publish a module: + +```rust +// resource_account.move +public entry fun create_resource_account_and_publish_package( + origin: &signer, + seed: vector, + metadata_serialized: vector, + code: vector>, +) acquires Container { + let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); + aptos_framework::code::publish_package_txn(&resource, metadata_serialized, code); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + resource_signer_cap, + ZERO_AUTH_KEY, + ); +} +``` + +:::warning Immutable Contracts +By default, publishing a module to a resource account means it will be immutable *unless* you store the SignerCapability somewhere in the `init_module` function. This is because the auth key is rotated to `ZERO_AUTH_KEY`, meaning the only way to control it is through a `SignerCapability`. + +If you don't store the `SignerCapability` there is no way to retrieve the resource account's signer, rendering it immutable. + +You *also* need to provide some way to use or retrieve the `SignerCapability`, too, or you won't even be able to use it. +::: + +### Publishing an upgradeable module with a resource account + +If you want to publish to a resource account and also have an upgradeable contract, use the `init_module` function to use the resource account's signer to retrieve and store the `SignerCapability`. Here's a full working example: + +```rust +module upgrade_resource_contract::upgrader { + use std::signer; + use std::account::{SignerCapability}; + use std::resource_account; + use std::account; + use std::code; + + struct MySignerCapability has key { + resource_signer_cap: SignerCapability, + } + + fun init_module(resource_signer: &signer) { + assert!(signer::address_of(resource_signer) == @upgrade_resource_contract, 0); + let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @owner); + move_to(resource_signer, MySignerCapability { + resource_signer_cap: resource_signer_cap, + }); + } + + // Note the assertion that the caller is @owner. If we leave this line out, anyone can upgrade the contract, exposing the resource account's resources and the contract functionality. + public entry fun upgrade_contract( + owner: &signer, + metadata_serialized: vector, + code: vector>, + ) acquires MySignerCapability { + assert!(signer::address_of(owner) == @owner, 1); + let resource_signer_cap = &borrow_global(@upgrade_resource_contract).resource_signer_cap; + let resource_signer = account::create_signer_with_capability(resource_signer_cap); + code::publish_package_txn( + &resource_signer, + metadata_serialized, + code, + ); + } +} +``` + +The `init_module` function is a special function that is called a single time upon the initial publication of a module. It inherently passes in the caller's `&signer`, which in our case is the resource account. This gives us a brief opportunity to store the `SignerCapability` somewhere. + +The `upgrade_contract` function takes in the owner as a signer and then borrows the resource signer cap, generates the resource account's signer, and publishes the package code from the input. Keep in mind you need to serialize the data for these two arguments correctly, or it won't work. + +Also note that the `retrieve_resource_account_cap` function takes in the source address as its second argument, so you need to somehow pass in the account address being used to create and publish. In our case, we used the named address `@owner` and specify it with an Aptos CLI profile: + +```shell +aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --named-addresses owner=CONTRACT_DEPLOYER --profile CONTRACT_DEPLOYER +``` + +Where `CONTRACT_DEPLOYER` is the profile. Read more about [Aptos CLI profiles here](https://aptos.dev/tools/aptos-cli-tool/use-aptos-cli/#creating-other-profiles). + +If you want to see an end to end unit test displaying how to publish and then upgrade the code above by calling `upgrade_contract`, you can run the cargo test: + +```shell +cargo test --package e2e-move-tests --lib -- tests::upgrade_resource_contract::code_upgrading_using_resource_account --exact --nocapture +``` + +### Creating and funding a resource account + +Another common usage is to create and fund a resource account, in case the account needs access to functions that need access to `Coin`: + +```rust +// resource_account.move +public entry fun create_resource_account_and_fund( + origin: &signer, + seed: vector, + optional_auth_key: vector, + fund_amount: u64, +) acquires Container { + let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); + coin::register(&resource); + coin::transfer(origin, signer::address_of(&resource), fund_amount); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + resource_signer_cap, + optional_auth_key, + ); +} +``` + +#### Can I acquire a SignerCapability later? + +Yes. Say you create a resource account and rotate its auth key to your account's auth key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: + +```rust +struct MySignerCapability has key { + resource_signer_cap: SignerCapability, +} + +public entry fun retrieve_cap(resource_signer: &signer, source_addr: address): acquires MySignerCapability { + let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, source_addr); + move_to(resource_signer, MySignerCapability { + resource_signer_cap, + }); +} +``` + +Call the function, but change the sender account to appear as the resource account with the CLI flag `--sender-account`. If the source address is the `default` profile: + +```shell +aptos move run --function-id MODULE_ADDRESS::MODULE_NAME::retrieve_cap --args address:default --sender-account RESOURCE_ADDRESS_HERE --profile default +``` + +#### How is the address for a resource account derived? + +When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address plus an optional byte vector `seed`. If you want to know the resource address generated by an account + a given arbitrary seed, you can call the `create_resource_address` function in `account.move`: + +```rust +account::create_resource_address(your_account_address, seed); +``` + +You can view the resource account functionality in more detail at [account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [resource_account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move). \ No newline at end of file diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index adbb1f3a932d5..c6c9d1e13c693 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -46,6 +46,7 @@ const sidebars = { }, "concepts/accounts", "concepts/resources", + "concepts/resource-accounts", "concepts/events", "concepts/txns-states", "concepts/gas-txn-fee", From 08662d632eae196a9f110dcb535ad7cab3ee7fda Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Sun, 11 Jun 2023 18:13:50 -0700 Subject: [PATCH 02/10] Adding a simple view function to demonstrate the upgradeability of the contract. Updated the tutorial to show how to use it. The tutorial shows how to use the newly added `build-publish-package` function to get the bytecode in a JSON file and run the upgrade_contract function with it. --- .../sources/upgrader.move | 5 + .../upgrade_contract.json | 16 ++ .../docs/concepts/resource-accounts.md | 152 ++++++++++++++++-- 3 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json diff --git a/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move b/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move index 03574402b819b..37f264a909082 100644 --- a/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move +++ b/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move @@ -32,4 +32,9 @@ module upgrade_resource_contract::upgrader { code, ); } + + #[view] + public fun upgradeable_function(): u64 { + 9000 + } } \ No newline at end of file diff --git a/aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json b/aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json new file mode 100644 index 0000000000000..2ab16336189c8 --- /dev/null +++ b/aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json @@ -0,0 +1,16 @@ +{ + "function_id": "be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9::upgrader::upgrade_contract", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x2155706772616465205265736f75726365204163636f756e7420436f6e747261637401000000000000000040423434313945414132343332454643453639443743433842333935364336454333363833344536343044434244314437343732323334363743434635414545329d011f8b08000000000002ff3d8dcd0ac2301084ef798a25775b5fc043117c00c15329b26ed6129a66437e1411dfdd04b1cc5c86f9861903d282334fcae3ca70007d097344c370e6242512c34024c567388acf11296bf5e098acf846efbb2aadca6f740de22cbd5a41b206ccf6e6582b351a0eec0d7bb29c2635842ce914ebe153e252e9373821746dd7757df5fd5ff6d8d8dd96357cbe9710eb1ab3000000010875706772616465729d041f8b08000000000002ff8d534d6fdb300cbdf7577018d0d94090a6c0306c4e5314d86987edb2e330188ccc24421dc9d347d2acc87f9fa428f2479d603ec9241f1ff9f4b49595ad096cb3565851a9484bab18954c0aa39099a2882905af37e03eab09b4a98a42f3b52035ef079131698543bdfe0ce9afd8e092d7dc1c8e83cac41421e38d0651262b9adf849836ca3203df0f4322d8a086673ac479fd97b84e33970c9b0286b849283f9ebaafac002eb829b7419f6cd0a180dbd321efb0a0d6a4ccbb2c961458550ea64bb91ac273582ce0e9a2e61398e5f3d4b62633b6002c6028a117d5284ebb4ecf98f288e1141378927b3f4dcbb5950e6be4dbca1199dbc52f4a3c129c24d431f246c5efeee0873404664351492e85fb4313420cebda7990eb38f414bead604f4e1ddc79904bd45c1048ebe4437190c263c4d9d8a71e495f7a69a4e6621dc2e729216af541a79076adaa1ed63b83f9d1d08b300da33776597306e42a0ec139e79b3d83b2b474983db9a715634b062b3458bac5b96bfd97aa0276c48c540ff6f3635be85f40cab405b122773bfcb1dc8d7ffdc6ae58f5648960d0709cc0fd7f99f176299592fb725dcb25d60f6fe91fb3cb8ecfa7234dafb23ac6e47aa6084d42eeb9d97878a4cd463a77160a7a16e106f5a66c903de3da3d811791f5ec7d3b7c11bdecc8edf50b3c4b1be91bfffdaf1da7fdefae933a1ec2654de5d973595e80fdf4b173915f66b3fbd8ecf80f9d1d74aacb05000000000300000000000000000000000000000000000000000000000000000000000000010e4170746f734672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200" + }, + { + "type": "hex", + "value": [ + "0xa11ceb0b060000000b01000a020a0803122305351d0752f30108c502400685034410c9032f0af803060cfe036e0dec04020000010101020103010400050800010a0600000600010000070201000008010300040b000500030c060400010d080700020e02010001060c0003060c0a020a0a020103010801010502060c05010c01060801087570677261646572076163636f756e7404636f6465107265736f757263655f6163636f756e74067369676e6572124d795369676e65724361706162696c6974790b696e69745f6d6f64756c6510757067726164655f636f6e7472616374147570677261646561626c655f66756e6374696f6e137265736f757263655f7369676e65725f636170105369676e65724361706162696c6974790a616464726573735f6f661d72657472696576655f7265736f757263655f6163636f756e745f6361701d6372656174655f7369676e65725f776974685f6361706162696c697479137075626c6973685f7061636b6167655f74786e5189d8b84793e93065f28893ab3e01cf5883e5daf93404d567a79be29958e423000000000000000000000000000000000000000000000000000000000000000105205189d8b84793e93065f28893ab3e01cf5883e5daf93404d567a79be29958e4230520f43c3dbad12a67d2c242a3e3bbbc8718cde26b36f75c075e96bf55dda9b0af76126170746f733a3a6d657461646174615f76311b000001147570677261646561626c655f66756e6374696f6e0101000002010908010000000004130a0011030700210406050a0b0001060000000000000000270a00070111040c010b000b0112002d0002010104010007120b001103070121040605080601000000000000002707002b00100011050c030e030b010b0211060202010000010206292300000000000002000000" + ] + } + ] +} \ No newline at end of file diff --git a/developer-docs-site/docs/concepts/resource-accounts.md b/developer-docs-site/docs/concepts/resource-accounts.md index 02f231a83ccc4..2c29ea78aa147 100644 --- a/developer-docs-site/docs/concepts/resource-accounts.md +++ b/developer-docs-site/docs/concepts/resource-accounts.md @@ -7,11 +7,14 @@ id: "resource-accounts" A resource account is an [Account](https://aptos.dev/concepts/accounts/) that's used to store and manage resources. It can be a simple storage account that's used merely to separate different resources for an account or a module, or it can be utilized to programmatically control resource management in a contract. -There are two distinct ways to manage a resource account. In this guide we'll discuss how to implement each technique, variations on the implementations, and any configuration details relevant to the creation process. +There are two distinct ways to manage a resource account: -## How to utilize a resource account +1. The auth key is rotated to a separate account that can control it manually +2. The Move VM rotates the auth key to 0x0 and controls the account through a SignerCapability -### Manually controlling a resource account by rotating its auth key to another account's auth key +In this guide we'll discuss how to implement each technique, variations on the implementations, and any configuration details relevant to the creation process. + +## Rotating the auth key to another account The first technique we're going to discuss is through the `create_resource_account` function in the `resource_account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). @@ -39,7 +42,7 @@ Notice that there is nothing returned here- we are not given anything to store o If you don't specify an auth key, that is, if you pass in `vector::empty()` or `vector []` to the `optional_auth_key` field, it will automatically rotate the auth key to the `origin` account's auth key. ::: -### Managing a resource account programmatically with a SignerCapability +## Rotating the auth key to 0x0 to create a SignerCapability The second technique is the `create_resource_account` function in the `account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). @@ -110,7 +113,7 @@ To intuitively understand why a `SignerCapability` is allowed to be so powerful, Upon creating the `SignerCapability`, you're free to decide how you want to expose it. You can store it somewhere, give it away, or gate its access to functions that use it or conditionally return it. ::: -### Using a resource account to publish a module +## Publishing a module to a resource account There are a few other ways we can utilize a resource account. One common usage is to use it to publish a module: @@ -141,7 +144,7 @@ If you don't store the `SignerCapability` there is no way to retrieve the resour You *also* need to provide some way to use or retrieve the `SignerCapability`, too, or you won't even be able to use it. ::: -### Publishing an upgradeable module with a resource account +## Publishing an upgradeable module to a resource account If you want to publish to a resource account and also have an upgradeable contract, use the `init_module` function to use the resource account's signer to retrieve and store the `SignerCapability`. Here's a full working example: @@ -180,6 +183,11 @@ module upgrade_resource_contract::upgrader { code, ); } + + #[view] + public fun upgradeable_function(): u64 { + 9000 + } } ``` @@ -195,13 +203,133 @@ aptos move create-resource-account-and-publish-package --address-name upgrade_re Where `CONTRACT_DEPLOYER` is the profile. Read more about [Aptos CLI profiles here](https://aptos.dev/tools/aptos-cli-tool/use-aptos-cli/#creating-other-profiles). -If you want to see an end to end unit test displaying how to publish and then upgrade the code above by calling `upgrade_contract`, you can run the cargo test: +Let's run through an example of how to publish the above upgradeable contract to a resource account and upgrade it. + +1. Publish the module to a resource account +2. Run the `upgradeable_function` view function and see what it returns +3. Upgrade the module using the json output from the `aptos move build-publish-package` command +4. Run the `upgradeable_function` view function again to see the new return value + +First make sure you have a default profile initialized to devnet. + +```shell +aptos init --profile default +``` + +Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `default` in our commands, it will automatically use this profile. + +Navigate to the `move-examples/upgrade_resource_contract` directory. + +### Publish the module + +```shell +aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --seed '' --named-addresses owner=default +``` + +The `--address-name` flag denotes that the resource address created from the resource account we make will be supplied as the `upgrade_resource_contract` address in our module. Since we declared it as the module address with `module upgrade_resource_contract::upgrader { ... }` at the very top of our contract, this is where our contract will be deployed. + +When you run this command, it will ask you something like this: + +``` +Do you want to publish this package under the resource account's address be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? [yes/no] > +``` + +Say yes and copy that address to your clipboard. That's our resource account address where the contract is deployed. + +Now you can run the view function! + +### Run the view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +``` + +Remember to replace `RESOURCE_ACCOUNT_ADDRESS` with the resource account address you deployed your module to; it is different from the one posted above, so this will specifically only work for *your* contract. + +It should output: +```json +Result: [ + 9000 +] +``` + +### Change the view function + +Now let's change the value returned in the view function from `9000` to `9001` so we can see that we've upgraded the contract: + +```rust +#[view] +public fun upgradeable_function(): u64 { + 9001 +} +``` + +Save that file, and then use the `build-publish-package` command to get the bytecode output in JSON format. + +### Get the bytecode for the module ```shell -cargo test --package e2e-move-tests --lib -- tests::upgrade_resource_contract::code_upgrading_using_resource_account --exact --nocapture +aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgrade_resource_contract=RESOURCE_ACCOUNT_ADDRESS,owner=default +``` + +Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. + +The hex values in this JSON file are arguments that we'd normally use to pass into the `0x1::code::publish_package_txn` function, but since we made our own `upgrade_contract` function that wraps it, we need to change the function call value to something else. + +Your JSON should look something like the below output, just with expanded `value` fields (truncated here for simplicity's sake): + +```json +{ + "function_id": "0x1::code::publish_package_txn", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x2155...6200" + }, + { + "type": "hex", + "value": [ + "0xa11c...0000" + ] + } + ] +} +``` + +Change the `function_id` value in the JSON file to match your contract's upgrade function contract, with your resource account address filled in: + +``` +"function_id": "RESOURCE_ACCOUNT_ADDRESS::upgrader::upgrade_contract", ``` -### Creating and funding a resource account +Save this file so we can use it to run an entry function with JSON parameters. + +### Run the upgrade_contract function + +```shell +aptos move run --json-file upgrade_contract.json +``` + +Confirm yes to publish the upgraded module where the view function will return 9001 instead of 9000. + +### Run the upgraded view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +``` + +You should get: + +```json +Result: [ + 9001 +] +``` + +Now you know how to publish an upgradeable module to a resource account! + +## Creating and funding a resource account Another common usage is to create and fund a resource account, in case the account needs access to functions that need access to `Coin`: @@ -225,9 +353,9 @@ public entry fun create_resource_account_and_fund( } ``` -#### Can I acquire a SignerCapability later? +## Acquiring a SignerCapability later -Yes. Say you create a resource account and rotate its auth key to your account's auth key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: +Say you create a resource account and rotate its auth key to your account's auth key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: ```rust struct MySignerCapability has key { @@ -248,7 +376,7 @@ Call the function, but change the sender account to appear as the resource accou aptos move run --function-id MODULE_ADDRESS::MODULE_NAME::retrieve_cap --args address:default --sender-account RESOURCE_ADDRESS_HERE --profile default ``` -#### How is the address for a resource account derived? +## How is the address for a resource account derived? When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address plus an optional byte vector `seed`. If you want to know the resource address generated by an account + a given arbitrary seed, you can call the `create_resource_address` function in `account.move`: From b709827eb7ffd4c07075c9f5c09f915e4313ef59 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 19 Jul 2023 23:41:04 -0700 Subject: [PATCH 03/10] Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to. --- .../docs/guides/resource-accounts/index.md | 9 ++ .../using-resource-accounts.md} | 82 +++++++++++-------- developer-docs-site/sidebars.js | 26 +++++- 3 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 developer-docs-site/docs/guides/resource-accounts/index.md rename developer-docs-site/docs/{concepts/resource-accounts.md => guides/resource-accounts/using-resource-accounts.md} (77%) diff --git a/developer-docs-site/docs/guides/resource-accounts/index.md b/developer-docs-site/docs/guides/resource-accounts/index.md new file mode 100644 index 0000000000000..0fdd50aa455d5 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/index.md @@ -0,0 +1,9 @@ +--- +title: "Resource Accounts" +--- + +# Examples of the various ways to use resource accounts + +- ### [Creating a resource account](./using-resource-accounts.md) +- ### [Using a resource account](./using-resource-accounts.md) +- ### [Okay blah blah blah](./using-resource-accounts.md) diff --git a/developer-docs-site/docs/concepts/resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md similarity index 77% rename from developer-docs-site/docs/concepts/resource-accounts.md rename to developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md index 2c29ea78aa147..1ab81fd3430b5 100644 --- a/developer-docs-site/docs/concepts/resource-accounts.md +++ b/developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md @@ -1,24 +1,36 @@ --- -title: "Resource Accounts" -id: "resource-accounts" +title: "Move subsections to individual pages" +id: "using-resource-accounts" --- -## What is a resource account? +# Utilizing Resource Accounts -A resource account is an [Account](https://aptos.dev/concepts/accounts/) that's used to store and manage resources. It can be a simple storage account that's used merely to separate different resources for an account or a module, or it can be utilized to programmatically control resource management in a contract. +### What is a resource account? + +A resource account is an [Account](https://aptos.dev/concepts/accounts/) that's used to store and manage resources. It can be used as a simple storage account for various resources or it can be utilized to programmatically manage resources from within a smart contract. There are two distinct ways to manage a resource account: -1. The auth key is rotated to a separate account that can control it manually -2. The Move VM rotates the auth key to 0x0 and controls the account through a SignerCapability +1. The authentication key is rotated to a separate account that can control it manually by signing for it +2. The authentication key is rotated to 0x0 and is controlled programmatically with a `SignerCapability` In this guide we'll discuss how to implement each technique, variations on the implementations, and any configuration details relevant to the creation process. -## Rotating the auth key to another account +You can view the smart contracts that define and use resource accounts in more detail at [account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [resource_account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move). + +:::tip +The `create_resource_account(...)` functions in `account.move` and `resource_account.move` are similar but serve two different purposes, each one reflecting one of the two aforementioned ways to manage resource accounts. + +The `create_resource_account(...)` function in `account.move` creates a resource account and rotates its authentication key to `0x0`. The resource account doesn't have an associated private key and can only be controlled through the usage of a [SignerCapability](#whats-a-signercapability). + +The `create_resource_account(...)` function in `resource_account.move` creates a resource account and rotates its authentication key to a specified authentication key. The account is still controlled by the specified authentication key's corresponding private key as long as the SignerCapability hasn't yet been retrieved. +::: + +## Rotating the authentication key to another account The first technique we're going to discuss is through the `create_resource_account` function in the `resource_account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). -You can specify a `seed` byte vector and an optional auth key to rotate the resulting resource account's auth key to. +You can specify a `seed` byte vector and an optional authentication key to rotate the resulting resource account's authentication key to. ```rust public entry fun create_resource_account( origin: &signer, @@ -34,15 +46,15 @@ public entry fun create_resource_account( ); } ``` -When you create a resource account like this, the account with the matching auth key can sign for it. Transactions signed in this way will show the signer/sender account as the resource account, but it was actually signed by the owning account with the matching auth key. This is mostly for separating resources into a different account- it's merely a way to organize and manage resources. +When you create a resource account like this, the account with the matching authentication key can sign for it. Transactions signed in this way will show the signer/sender account as the resource account, but it was actually signed by the owning account with the matching authentication key. This is mostly for separating resources into a different account- it's merely a way to organize and manage resources. -Notice that there is nothing returned here- we are not given anything to store or manage. We simply created a resource account and rotated its auth key to the optional auth key. +Notice that there is nothing returned here- we are not given anything to store or manage. We simply created a resource account and rotated its authentication key to the optional authentication key. :::tip -If you don't specify an auth key, that is, if you pass in `vector::empty()` or `vector []` to the `optional_auth_key` field, it will automatically rotate the auth key to the `origin` account's auth key. +If you don't specify an authentication key, that is, if you pass in `vector::empty()` or `vector []` to the `optional_auth_key` field, it will automatically rotate the authentication key to the `origin` account's authentication key. ::: -## Rotating the auth key to 0x0 to create a SignerCapability +## Rotating the authentication key to 0x0 to create a SignerCapability The second technique is the `create_resource_account` function in the `account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). @@ -55,25 +67,11 @@ public fun create_resource_account( } ``` -When this function is called, the auth key of the resource account is rotated to `0x0`, which gives the Move VM the capability to generate the resource account's signer from a `SignerCapability`. You can store this `SignerCapability` and retrieve it later to sign for the resource account. +When this function is called, the authentication key of the resource account is rotated to `0x0`, which gives the Move VM the capability to generate the resource account's signer from a `SignerCapability`. You can store this `SignerCapability` and retrieve it later to sign for the resource account. This is often integral to automating a smart contract in Move. It gives the developer the ability to generate a signer for an account programmatically. -Notice that the creation function returns the resource account's `signer` and a `SignerCapability` resource. Let's discuss what a `SignerCapability` is and then how to store it. - -#### What's a SignerCapability? - -A SignerCapability is a very simple resource, mostly meant to abstractly represent the ability to sign for an account. It doesn't actually *do* anything special, but its existence somewhere implies that if you have access to it, you either created it or received access to it very intentionally. - -It contains a single field called `account` the matches the address it's intended to generate a signature for: - -```rust -struct SignerCapability has drop, store { - account: address -} -``` - -Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. +Notice that the creation function returns the resource account's `signer` and a `SignerCapability` resource. Read more about what a `SignerCapability` is [here](#whats-a-signercapability). Here is a very basic example that demonstrates how you'd use a `SignerCapability` in a Move contract: @@ -137,7 +135,7 @@ public entry fun create_resource_account_and_publish_package( ``` :::warning Immutable Contracts -By default, publishing a module to a resource account means it will be immutable *unless* you store the SignerCapability somewhere in the `init_module` function. This is because the auth key is rotated to `ZERO_AUTH_KEY`, meaning the only way to control it is through a `SignerCapability`. +By default, publishing a module to a resource account means it will be immutable *unless* you store the SignerCapability somewhere in the `init_module` function. This is because the authentication key is rotated to `ZERO_AUTH_KEY`, meaning the only way to control it is through a `SignerCapability`. If you don't store the `SignerCapability` there is no way to retrieve the resource account's signer, rendering it immutable. @@ -355,7 +353,7 @@ public entry fun create_resource_account_and_fund( ## Acquiring a SignerCapability later -Say you create a resource account and rotate its auth key to your account's auth key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: +Say you create a resource account and rotate its authentication key to your account's authentication key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: ```rust struct MySignerCapability has key { @@ -378,10 +376,28 @@ aptos move run --function-id MODULE_ADDRESS::MODULE_NAME::retrieve_cap --args ad ## How is the address for a resource account derived? -When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address plus an optional byte vector `seed`. If you want to know the resource address generated by an account + a given arbitrary seed, you can call the `create_resource_address` function in `account.move`: +When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address plus an optional byte vector `seed`. If you want to know the resource address generated by an account and a given seed, you can call the `create_resource_address` function in `account.move`: +```rust +/// This is a helper function to compute resource addresses. Computation of the address +/// involves the use of a cryptographic hash operation and should be use thoughtfully. +public fun create_resource_address(source: &address, seed: vector): address { + let bytes = bcs::to_bytes(source); + vector::append(&mut bytes, seed); + vector::push_back(&mut bytes, DERIVE_RESOURCE_ACCOUNT_SCHEME); + from_bcs::to_address(hash::sha3_256(bytes)) +} +``` + +## What's a SignerCapability? + +A SignerCapability is a very simple resource, intended to be an abstract representation of the ability to sign for an account. It doesn't actually *do* anything special, but its existence somewhere implies that if you have access to it, you either created it or received access to it very intentionally. + +It contains a single field called `account` the matches the address it's intended to generate a signature for: ```rust -account::create_resource_address(your_account_address, seed); +struct SignerCapability has drop, store { + account: address +} ``` -You can view the resource account functionality in more detail at [account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [resource_account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move). \ No newline at end of file +Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index c6c9d1e13c693..63bfe9672eb30 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -46,7 +46,6 @@ const sidebars = { }, "concepts/accounts", "concepts/resources", - "concepts/resource-accounts", "concepts/events", "concepts/txns-states", "concepts/gas-txn-fee", @@ -337,6 +336,11 @@ const sidebars = { }, { type: "category", +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +>>>>>>> 2cc005c6a5 (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) label: "Examples", collapsible: true, collapsed: true, @@ -347,10 +351,30 @@ const sidebars = { slug: "/category/examples", keywords: ["examples"], }, +<<<<<<< HEAD items: ["guides/account-management/key-rotation"], }, { type: "category", +======= + items: [ + "guides/account-management/key-rotation", + { + type: "category", + label: "Resource accounts", + link: { type: "doc", id: "guides/resource-accounts/index" }, + collapsible: true, + collapsed: true, + items: [ + "guides/resource-accounts/using-resource-accounts", + ], + }, + ], + }, + { + type: "category", +>>>>>>> 55c0f3ab0c (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) +>>>>>>> 2cc005c6a5 (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) label: "Build E2E Dapp with Aptos", link: { type: "doc", id: "tutorials/build-e2e-dapp/index" }, collapsible: true, From 04a52e3f6d49ae653cd6ba97700fbc66dbe38c56 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:36:33 -0700 Subject: [PATCH 04/10] Upgrading the rust unit tests for the package_manager/upgradeable resource contract. --- aptos-move/e2e-move-tests/src/tests/mod.rs | 2 +- .../src/tests/upgrade_resource_contract.rs | 66 --------- .../tests/upgradeable_resource_contract.rs | 135 ++++++++++++++++++ .../sources/upgrader.move | 40 ------ .../Move.toml | 7 +- .../sources/basic_contract.move | 6 + .../sources/package_manager.move | 78 ++++++++++ .../upgrade_contract.json | 0 8 files changed, 226 insertions(+), 108 deletions(-) delete mode 100644 aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs create mode 100644 aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs delete mode 100644 aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move rename aptos-move/move-examples/{upgrade_resource_contract => upgradeable_resource_contract}/Move.toml (50%) create mode 100644 aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move create mode 100644 aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move rename aptos-move/move-examples/{upgrade_resource_contract => upgradeable_resource_contract}/upgrade_contract.json (100%) diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index 6ae31d6b150b5..bd22dafdb7df4 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -39,6 +39,6 @@ mod token_event_store; mod token_objects; mod transaction_fee; mod type_too_large; -mod upgrade_resource_contract; +mod upgradeable_resource_contract; mod vector_numeric_address; mod vote; diff --git a/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs b/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs deleted file mode 100644 index e2812ad77652a..0000000000000 --- a/aptos-move/e2e-move-tests/src/tests/upgrade_resource_contract.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -use crate::{assert_success, tests::common, MoveHarness}; -use aptos_types::{ - account_address::{create_resource_address, AccountAddress}, -}; - -// Note: this module uses parameterized tests via the -// [`rstest` crate](https://crates.io/crates/rstest) -// to test for multiple feature combinations. - -#[test] -fn code_upgrading_using_resource_account() { - let mut h = MoveHarness::new(); - - let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); - let resource_address = create_resource_address(*acc.address(), &[]); - - // add the named addresses for `owner` and `upgrade_resource_contract` - let mut build_options = aptos_framework::BuildOptions::default(); - build_options - .named_addresses - .insert("owner".to_string(), *acc.address()); - build_options - .named_addresses - .insert("upgrade_resource_contract".to_string(), resource_address); - - // build the package from our example code - let package = aptos_framework::BuiltPackage::build( - common::test_dir_path("../../../move-examples/upgrade_resource_contract"), - build_options, - ) - .expect("building package must succeed"); - - let code = package.extract_code(); - let metadata = package - .extract_metadata() - .expect("extracting package metadata must succeed"); - - // create the resource account and publish the module under the resource account's address - let result = h.run_transaction_payload( - &acc, - aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account_and_publish_package( - vec![], - bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), - code.clone(), - ), - ); - - assert_success!(result); - - // test upgrading the code - assert_success!(h.run_entry_function( - &acc, - str::parse(&format!( - "0x{}::upgrader::upgrade_contract", - resource_address - )).unwrap(), - vec![], - vec![ - bcs::to_bytes(&bcs::to_bytes(&metadata).unwrap()).unwrap(), - bcs::to_bytes(&code).unwrap(), - ], - )); -} diff --git a/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs new file mode 100644 index 0000000000000..b60ba07db6ee6 --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs @@ -0,0 +1,135 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::{assert_success, tests::common, MoveHarness}; +use aptos_package_builder::PackageBuilder; +use aptos_types::account_address::{create_resource_address, AccountAddress}; +use aptos_framework::natives::code::{PackageMetadata, UpgradePolicy}; + +fn custom_build_helper( + deployer_address: AccountAddress, + resource_address: AccountAddress, + package_manager_code: &String, + basic_contract_code: &String, +) -> (PackageMetadata, Vec>) { + // add the named addresses for `deployer` and `upgradeable_resource_contract` + let mut build_options = aptos_framework::BuildOptions::default(); + build_options + .named_addresses + .insert("deployer".to_string(), deployer_address); + build_options + .named_addresses + .insert("upgradeable_resource_contract".to_string(), resource_address); + + let mut package_builder = + PackageBuilder::new("Upgradeable Resource Account Contract") + .with_policy(UpgradePolicy::compat()); + package_builder.add_source("package_manager", &package_manager_code); + package_builder.add_source("basic_contract", &basic_contract_code); + package_builder.add_local_dep("AptosFramework", &common::framework_dir_path("aptos-framework").to_string_lossy()); + let pack_dir = package_builder.write_to_temp().unwrap(); + let package = aptos_framework::BuiltPackage::build( + pack_dir.path().to_owned(), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_code(); + let metadata = package + .extract_metadata() + .expect("extracting package metadata must succeed"); + + (metadata, code) +} + +#[test] +fn code_upgrading_using_resource_account() { + let mut h = MoveHarness::new(); + + let deployer = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + let resource_address = create_resource_address(*deployer.address(), &[]); + + // add the named addresses for `deployer` and `upgradeable_resource_contract` + let mut build_options = aptos_framework::BuildOptions::default(); + build_options + .named_addresses + .insert("deployer".to_string(), *deployer.address()); + build_options + .named_addresses + .insert("upgradeable_resource_contract".to_string(), resource_address); + + // get contract code from file + let package_manager_code = + std::fs::read_to_string( + &common::test_dir_path("../../../move-examples/upgradeable_resource_contract/sources/package_manager.move") + ).unwrap(); + let basic_contract_code = + std::fs::read_to_string( + &common::test_dir_path("../../../move-examples/upgradeable_resource_contract/sources/basic_contract.move") + ).unwrap(); + + let (metadata, code) = custom_build_helper( + *deployer.address(), + resource_address, + &package_manager_code, + &basic_contract_code + ); + + // create the resource account and publish the module under the resource account's address + assert_success!(h.run_transaction_payload( + &deployer, + aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account_and_publish_package( + vec![], + bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), + code.clone(), + ), + )); + + // run the view function and check the result + let bcs_result = h.execute_view_function( + str::parse(&format!( + "0x{}::basic_contract::upgradeable_function", + resource_address + )).unwrap(), + vec![], + vec![], + ).unwrap().pop().unwrap(); + let result = bcs::from_bytes::(&bcs_result).unwrap(); + + const BEFORE_VALUE: u64 = 9000; + const AFTER_VALUE: u64 = 9001; + // run the view function and check the result + assert_eq!(BEFORE_VALUE, result, "assert view function result {} == {}", result, BEFORE_VALUE); + + let (metadata, code) = custom_build_helper( + *deployer.address(), + resource_address, + &package_manager_code, + &basic_contract_code.replace(&BEFORE_VALUE.to_string(), &AFTER_VALUE.to_string()) + ); + // test upgrading the code + assert_success!(h.run_entry_function( + &deployer, + str::parse(&format!( + "0x{}::package_manager::publish_package", + resource_address + )).unwrap(), + vec![], + vec![ + bcs::to_bytes(&bcs::to_bytes(&metadata).unwrap()).unwrap(), + bcs::to_bytes(&code).unwrap(), + ], + )); + + // run the view function and check the result + let bcs_result = h.execute_view_function( + str::parse(&format!( + "0x{}::basic_contract::upgradeable_function", + resource_address + )).unwrap(), + vec![], + vec![], + ).unwrap().pop().unwrap(); + let result = bcs::from_bytes::(&bcs_result).unwrap(); + assert_eq!(AFTER_VALUE, result, "assert view function result {} == {}", result, AFTER_VALUE); +} diff --git a/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move b/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move deleted file mode 100644 index 37f264a909082..0000000000000 --- a/aptos-move/move-examples/upgrade_resource_contract/sources/upgrader.move +++ /dev/null @@ -1,40 +0,0 @@ -module upgrade_resource_contract::upgrader { - use std::signer; - use std::account::{SignerCapability}; - use std::resource_account; - use std::account; - use std::code; - - struct MySignerCapability has key { - resource_signer_cap: SignerCapability, - } - - fun init_module(resource_signer: &signer) { - assert!(signer::address_of(resource_signer) == @upgrade_resource_contract, 0); - let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @owner); - move_to(resource_signer, MySignerCapability { - resource_signer_cap: resource_signer_cap, - }); - } - - // Note the assertion that the caller is @owner. If we leave this line out, anyone can upgrade the contract, exposing the resource account's resources and the contract functionality. - public entry fun upgrade_contract( - owner: &signer, - metadata_serialized: vector, - code: vector>, - ) acquires MySignerCapability { - assert!(signer::address_of(owner) == @owner, 1); - let resource_signer_cap = &borrow_global(@upgrade_resource_contract).resource_signer_cap; - let resource_signer = account::create_signer_with_capability(resource_signer_cap); - code::publish_package_txn( - &resource_signer, - metadata_serialized, - code, - ); - } - - #[view] - public fun upgradeable_function(): u64 { - 9000 - } -} \ No newline at end of file diff --git a/aptos-move/move-examples/upgrade_resource_contract/Move.toml b/aptos-move/move-examples/upgradeable_resource_contract/Move.toml similarity index 50% rename from aptos-move/move-examples/upgrade_resource_contract/Move.toml rename to aptos-move/move-examples/upgradeable_resource_contract/Move.toml index 4a993944139d9..09c2e38fc268a 100644 --- a/aptos-move/move-examples/upgrade_resource_contract/Move.toml +++ b/aptos-move/move-examples/upgradeable_resource_contract/Move.toml @@ -1,7 +1,12 @@ [package] -name = "Upgrade Resource Account Contract" +name = "Upgradeable Resource Account Contract" version = "0.0.0" upgrade_policy = "compatible" +[addresses] +aptos_framework = "0x1" +deployer = "_" +upgradeable_resource_contract = "_" + [dependencies] AptosFramework = { local = "../../framework/aptos-framework" } \ No newline at end of file diff --git a/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move b/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move new file mode 100644 index 0000000000000..5aa03383b3406 --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move @@ -0,0 +1,6 @@ +module upgradeable_resource_contract::basic_contract { + #[view] + public fun upgradeable_function(): u64 { + 9000 + } +} diff --git a/aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move b/aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move new file mode 100644 index 0000000000000..e0e65ef3fd797 --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move @@ -0,0 +1,78 @@ +module upgradeable_resource_contract::package_manager { + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::resource_account; + use aptos_std::smart_table::{Self, SmartTable}; + use std::string::String; + use aptos_std::code; + use std::error; + use std::signer; + + /// The signer is not authorized to deploy this module. + const ENOT_AUTHORIZED: u64 = 0; + + /// Stores permission config such as SignerCapability for controlling the resource account. + struct PermissionConfig has key { + /// Required to obtain the resource account signer. + signer_cap: SignerCapability, + /// Track the addresses created by the modules in this package. + addresses: SmartTable, + } + + /// Initialize PermissionConfig to establish control over the resource account. + /// This function is invoked only when this package is deployed the first time. + fun init_module(resource_signer: &signer) { + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @deployer); + move_to(resource_signer, PermissionConfig { + addresses: smart_table::new(), + signer_cap, + }); + } + + public entry fun publish_package( + deployer: &signer, + package_metadata: vector, + code: vector>, + ) acquires PermissionConfig { + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + code::publish_package_txn(&get_signer(), package_metadata, code); + } + + /// Can be called by friended modules to obtain the resource account signer. + public(friend) fun get_signer(): signer acquires PermissionConfig { + let signer_cap = &borrow_global(@upgradeable_resource_contract).signer_cap; + account::create_signer_with_capability(signer_cap) + } + + /// Can be called by friended modules to keep track of a system address. + public(friend) fun add_address(name: String, object: address) acquires PermissionConfig { + let addresses = &mut borrow_global_mut(@upgradeable_resource_contract).addresses; + smart_table::add(addresses, name, object); + } + + public fun address_exists(name: String): bool acquires PermissionConfig { + smart_table::contains(&safe_permission_config().addresses, name) + } + + public fun get_address(name: String): address acquires PermissionConfig { + let addresses = &borrow_global(@upgradeable_resource_contract).addresses; + *smart_table::borrow(addresses, name) + } + + inline fun safe_permission_config(): &PermissionConfig acquires PermissionConfig { + borrow_global(@upgradeable_resource_contract) + } + + #[test_only] + public fun initialize_for_test(deployer: &signer) { + let deployer_addr = std::signer::address_of(deployer); + if (!exists(deployer_addr)) { + aptos_framework::timestamp::set_time_has_started_for_testing(&account::create_signer_for_test(@0x1)); + + account::create_account_for_test(deployer_addr); + move_to(deployer, PermissionConfig { + addresses: smart_table::new(), + signer_cap: account::create_test_signer_cap(deployer_addr), + }); + }; + } +} diff --git a/aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json b/aptos-move/move-examples/upgradeable_resource_contract/upgrade_contract.json similarity index 100% rename from aptos-move/move-examples/upgrade_resource_contract/upgrade_contract.json rename to aptos-move/move-examples/upgradeable_resource_contract/upgrade_contract.json From c1ef36478cad87a94d22b8fa7249272028c717cd Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Thu, 20 Jul 2023 23:39:34 -0700 Subject: [PATCH 05/10] Updating the tutorial/guide for resource accounts. About halfway through revising everything. Still need to add kevin's package manager and explanation into the tutorial. --- .../docs/guides/resource-accounts/index.md | 5 +- .../understanding-resource-accounts.md | 134 ++++++ .../using-resource-accounts.md | 403 ------------------ .../utilizing-resource-accounts.md | 342 +++++++++++++++ developer-docs-site/sidebars.js | 3 +- 5 files changed, 480 insertions(+), 407 deletions(-) create mode 100644 developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md delete mode 100644 developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md create mode 100644 developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md diff --git a/developer-docs-site/docs/guides/resource-accounts/index.md b/developer-docs-site/docs/guides/resource-accounts/index.md index 0fdd50aa455d5..205699c3b531b 100644 --- a/developer-docs-site/docs/guides/resource-accounts/index.md +++ b/developer-docs-site/docs/guides/resource-accounts/index.md @@ -4,6 +4,5 @@ title: "Resource Accounts" # Examples of the various ways to use resource accounts -- ### [Creating a resource account](./using-resource-accounts.md) -- ### [Using a resource account](./using-resource-accounts.md) -- ### [Okay blah blah blah](./using-resource-accounts.md) +- ### [Utilizing resource accounts](./utilizing-resource-accounts) +- ### [Understanding resource accounts](./understanding-resource-accounts) diff --git a/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md new file mode 100644 index 0000000000000..096625b7d6603 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md @@ -0,0 +1,134 @@ +--- +title: "Conceptual understanding" +id: "understanding-resource-accounts" +--- + +# Conceptual understanding + +## What is a resource account? + +A [resource account](../../move/move-on-aptos/resource-accounts.md) is an [Account](../../concepts/accounts/) that's used to store and manage resources independent of a user. It can be used as a simple storage account or it can be utilized to programmatically manage resources from within a smart contract. + +## How are resource accounts created? + +Let's review the two functions used to create resource accounts and what they return. + +First off, note that both creation functions allow for the input of a `seed` byte vector for the [ensuing hash used to compute the resource address](#how-is-the-address-for-a-resource-account-derived). + + + + +The `account.move` version creates a resource account and rotates its authentication key to `0x0`. The resulting resource account doesn't have an associated private key and can only be controlled through the usage of the [SignerCapability](#whats-a-signercapability) returned by the creation function. + +```rust title="Creating a resource account in account.move" +public fun create_resource_account( + source: &signer, + seed: vector +): (signer, SignerCapability) acquires Account { + let resource_addr = create_resource_address(&signer::address_of(source), seed); + + // ... + + // By default, only the SignerCapability should have control over the resource account and not the auth key. + // If the source account wants direct control via auth key, they would need to explicitly rotate the auth key + // of the resource account using the SignerCapability. + rotate_authentication_key_internal(&resource, ZERO_AUTH_KEY); + + let account = borrow_global_mut(resource_addr); + account.signer_capability_offer.for = option::some(resource_addr); + let signer_cap = SignerCapability { account: resource_addr }; + (resource, signer_cap) +} +``` + + + +The `create_resource_account` function in `resource_account.move` below creates a resource account and rotates its authentication key to the `optional_auth_key` argument. If this field is an empty vector, the authentication key is rotated to the `origin` account's authentication key. + +```rust title="Creating a manually controlled resource account in resource_account.move" +public entry fun create_resource_account( + origin: &signer, + seed: vector, + optional_auth_key: vector, +) acquires Container { + let (resource, signer_cap) = account::create_resource_account(origin, seed); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + signer_cap, + optional_auth_key, + ); +} +``` +The resource account created from this is functionally very similar to a user account that has had its authentication key rotated (see: [Rotating an authentication key](../account-management/key-rotation.md)), because it cannot yet be controlled programmatically and can still be controlled by a private key. + +However, there does exist a SignerCapability for the resource account, it just isn't being used yet. To enable programmatic control, you would need to [retrieve the SignerCapability.](./utilizing-resource-accounts#retrieving-a-signercapability) + +The end result of a creating a resource account with `create_resource_account(...)` in `resource_account.move` and then retrieving the SignerCapability is the same as creating the resource account with `create_resource_account(...)` in `account.move`. + + + +## What's a SignerCapability? + +A SignerCapability is a simple but powerful resource that allows a developer to programmatically manage a resource account. This is achieved with the `create_signer_with_capability` function: + +```rust +public fun create_signer_with_capability(capability: &SignerCapability): signer { + let addr = &capability.account; + create_signer(*addr) +} +``` + +The SignerCapability resource doesn't actually *do* anything special on its own, it's juts an abstract representation of permission to generate a [`signer`](../../move/book/signer.md) primitive for the account it was created for. + +It contains a single field called `account`, which is just the address that it has permission to generate a `signer` for: + +```rust +struct SignerCapability has drop, store { + account: address +} +``` + +Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. + + +## What's stopping someone from using my SignerCapability? + +You might be wondering "*Why does this work? Isn't it dangerous to be able to create a signer for an account so easily?*" + +Move's [privileged struct operations](../../move/book/structs-and-resources#privileged-struct-operations) require that creating structs and accessing their inner fields can only occur from within the module that defines the struct. This means that unless the developer provides a public accessor function to a stored `SignerCapability`, there is no way for another account to gain access to it. + +:::warning +Be mindful of properly gating access to a function that uses a `SignerCapability` and be extra careful when returning it from a function, since this gives the caller unrestricted access to control the account. +::: + +A good rule of thumb is to by default set all functions that return a `SignerCapability` or a `signer` to internal private functions unless you've very carefully thought about the implications. + +```rust title="An example of a private function that returns a signer" +use aptos_std::resource_account; + +// The function name `internal_get_signer()` is functionally no different than +// `get_signer()`, but it signifies to any developer that comes across it that +// it shouldn't be publically accessible without careful thought. +fun internal_get_signer(): signer { + // Borrow the signer cap you've stored somewhere + let signer_cap = borrow_global(@your_contract).signer_cap; + + // Return the signer generated from it + resource_account::create_signer_with_capability(&signer_cap) +} +``` + +## How is the address for a resource account derived? + +When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address, a byte scheme to identify it as a resource account, and an optional user-specified byte vector `seed`. Here is the implementation of `create_resource_address` function in `account.move`: +```rust +/// This is a helper function to compute resource addresses. Computation of the address +/// involves the use of a cryptographic hash operation and should be use thoughtfully. +public fun create_resource_address(source: &address, seed: vector): address { + let bytes = bcs::to_bytes(source); + vector::append(&mut bytes, seed); + vector::push_back(&mut bytes, DERIVE_RESOURCE_ACCOUNT_SCHEME); + from_bcs::to_address(hash::sha3_256(bytes)) +} +``` diff --git a/developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md deleted file mode 100644 index 1ab81fd3430b5..0000000000000 --- a/developer-docs-site/docs/guides/resource-accounts/using-resource-accounts.md +++ /dev/null @@ -1,403 +0,0 @@ ---- -title: "Move subsections to individual pages" -id: "using-resource-accounts" ---- - -# Utilizing Resource Accounts - -### What is a resource account? - -A resource account is an [Account](https://aptos.dev/concepts/accounts/) that's used to store and manage resources. It can be used as a simple storage account for various resources or it can be utilized to programmatically manage resources from within a smart contract. - -There are two distinct ways to manage a resource account: - -1. The authentication key is rotated to a separate account that can control it manually by signing for it -2. The authentication key is rotated to 0x0 and is controlled programmatically with a `SignerCapability` - -In this guide we'll discuss how to implement each technique, variations on the implementations, and any configuration details relevant to the creation process. - -You can view the smart contracts that define and use resource accounts in more detail at [account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [resource_account.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move). - -:::tip -The `create_resource_account(...)` functions in `account.move` and `resource_account.move` are similar but serve two different purposes, each one reflecting one of the two aforementioned ways to manage resource accounts. - -The `create_resource_account(...)` function in `account.move` creates a resource account and rotates its authentication key to `0x0`. The resource account doesn't have an associated private key and can only be controlled through the usage of a [SignerCapability](#whats-a-signercapability). - -The `create_resource_account(...)` function in `resource_account.move` creates a resource account and rotates its authentication key to a specified authentication key. The account is still controlled by the specified authentication key's corresponding private key as long as the SignerCapability hasn't yet been retrieved. -::: - -## Rotating the authentication key to another account - -The first technique we're going to discuss is through the `create_resource_account` function in the `resource_account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). - -You can specify a `seed` byte vector and an optional authentication key to rotate the resulting resource account's authentication key to. -```rust -public entry fun create_resource_account( - origin: &signer, - seed: vector, - optional_auth_key: vector, -) acquires Container { - let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); - rotate_account_authentication_key_and_store_capability( - origin, - resource, - resource_signer_cap, - optional_auth_key, - ); -} -``` -When you create a resource account like this, the account with the matching authentication key can sign for it. Transactions signed in this way will show the signer/sender account as the resource account, but it was actually signed by the owning account with the matching authentication key. This is mostly for separating resources into a different account- it's merely a way to organize and manage resources. - -Notice that there is nothing returned here- we are not given anything to store or manage. We simply created a resource account and rotated its authentication key to the optional authentication key. - -:::tip -If you don't specify an authentication key, that is, if you pass in `vector::empty()` or `vector []` to the `optional_auth_key` field, it will automatically rotate the authentication key to the `origin` account's authentication key. -::: - -## Rotating the authentication key to 0x0 to create a SignerCapability - -The second technique is the `create_resource_account` function in the `account.move` contract. View the code [here](https://github.com/aptos-labs/aptos-core/blob/4beb914a168bd358ec375bfd9854cffaa271199a/aptos-move/framework/aptos-framework/sources/account.move#L602). - -```rust -public fun create_resource_account( - source: &signer, - seed: vector -): (signer, SignerCapability) acquires Account { - // ... -} -``` - -When this function is called, the authentication key of the resource account is rotated to `0x0`, which gives the Move VM the capability to generate the resource account's signer from a `SignerCapability`. You can store this `SignerCapability` and retrieve it later to sign for the resource account. - -This is often integral to automating a smart contract in Move. It gives the developer the ability to generate a signer for an account programmatically. - -Notice that the creation function returns the resource account's `signer` and a `SignerCapability` resource. Read more about what a `SignerCapability` is [here](#whats-a-signercapability). - -Here is a very basic example that demonstrates how you'd use a `SignerCapability` in a Move contract: - -```rust -// define a resource we can store the SignerCapability in. We use key here for simplicity's sake -struct MySignerCapability has key { - resource_signer_cap: SignerCapability, -} - -public entry fun store_signer_capability(creator: &signer) { - // We store `MySignerCapability` to an account's resources. We can even store it on the resource account itself: - let (resource_signer, resource_signer_cap) = account::create_resource_account(creator, b"seed bytes"); - move_to(resource_signer, MySignerCapability { - resource_signer_cap, - }); -} - -// Now we utilize the resource account by generating its signer with the SignerCapability -public entry fun sign_with_resource_account(creator: &signer) acquires MySignerCapability { - let resource_address = account::create_resource_address(signer::address_of(creator), b"seed bytes"); - let signer_cap = borrow_global(resource_account_address); - let resource_signer = account::create_signer_with_capability(signer_cap); - - // here we'd do something with the resource_signer that we can only do with its `signer`, like transfer coins, create/transfer an NFT, or call some other function that rqeuires a signer. - // be careful with making functions like these entry functions. If you have no contingencies for a function like this, they can be very easily abused. -} -``` -Utilizing a resource account in this way is the fundamental process for automating the generation and retrieval of resources on-chain. - -You might be wondering "*Why does this work? Isn't it dangerous to be able to create a signer for an account so easily?*" - -Yes, you need to make sure you're gating access to a `SignerCapability` whenever you store it somewhere. Be very thoughtful with how you facilitate access to one, because unrestricted access to it gives free reign for anyone to call any function that requires a signer with it. - -:::tip -To intuitively understand why a `SignerCapability` is allowed to be so powerful, you need to consider how resource storage and control work in Move. You can't directly access, create, or modify a resource outside of the module it's defined in, meaning if you have access to a resource in some way, the creator of the module it belongs to explicitly gave it to you. - -Upon creating the `SignerCapability`, you're free to decide how you want to expose it. You can store it somewhere, give it away, or gate its access to functions that use it or conditionally return it. -::: - -## Publishing a module to a resource account - -There are a few other ways we can utilize a resource account. One common usage is to use it to publish a module: - -```rust -// resource_account.move -public entry fun create_resource_account_and_publish_package( - origin: &signer, - seed: vector, - metadata_serialized: vector, - code: vector>, -) acquires Container { - let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); - aptos_framework::code::publish_package_txn(&resource, metadata_serialized, code); - rotate_account_authentication_key_and_store_capability( - origin, - resource, - resource_signer_cap, - ZERO_AUTH_KEY, - ); -} -``` - -:::warning Immutable Contracts -By default, publishing a module to a resource account means it will be immutable *unless* you store the SignerCapability somewhere in the `init_module` function. This is because the authentication key is rotated to `ZERO_AUTH_KEY`, meaning the only way to control it is through a `SignerCapability`. - -If you don't store the `SignerCapability` there is no way to retrieve the resource account's signer, rendering it immutable. - -You *also* need to provide some way to use or retrieve the `SignerCapability`, too, or you won't even be able to use it. -::: - -## Publishing an upgradeable module to a resource account - -If you want to publish to a resource account and also have an upgradeable contract, use the `init_module` function to use the resource account's signer to retrieve and store the `SignerCapability`. Here's a full working example: - -```rust -module upgrade_resource_contract::upgrader { - use std::signer; - use std::account::{SignerCapability}; - use std::resource_account; - use std::account; - use std::code; - - struct MySignerCapability has key { - resource_signer_cap: SignerCapability, - } - - fun init_module(resource_signer: &signer) { - assert!(signer::address_of(resource_signer) == @upgrade_resource_contract, 0); - let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @owner); - move_to(resource_signer, MySignerCapability { - resource_signer_cap: resource_signer_cap, - }); - } - - // Note the assertion that the caller is @owner. If we leave this line out, anyone can upgrade the contract, exposing the resource account's resources and the contract functionality. - public entry fun upgrade_contract( - owner: &signer, - metadata_serialized: vector, - code: vector>, - ) acquires MySignerCapability { - assert!(signer::address_of(owner) == @owner, 1); - let resource_signer_cap = &borrow_global(@upgrade_resource_contract).resource_signer_cap; - let resource_signer = account::create_signer_with_capability(resource_signer_cap); - code::publish_package_txn( - &resource_signer, - metadata_serialized, - code, - ); - } - - #[view] - public fun upgradeable_function(): u64 { - 9000 - } -} -``` - -The `init_module` function is a special function that is called a single time upon the initial publication of a module. It inherently passes in the caller's `&signer`, which in our case is the resource account. This gives us a brief opportunity to store the `SignerCapability` somewhere. - -The `upgrade_contract` function takes in the owner as a signer and then borrows the resource signer cap, generates the resource account's signer, and publishes the package code from the input. Keep in mind you need to serialize the data for these two arguments correctly, or it won't work. - -Also note that the `retrieve_resource_account_cap` function takes in the source address as its second argument, so you need to somehow pass in the account address being used to create and publish. In our case, we used the named address `@owner` and specify it with an Aptos CLI profile: - -```shell -aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --named-addresses owner=CONTRACT_DEPLOYER --profile CONTRACT_DEPLOYER -``` - -Where `CONTRACT_DEPLOYER` is the profile. Read more about [Aptos CLI profiles here](https://aptos.dev/tools/aptos-cli-tool/use-aptos-cli/#creating-other-profiles). - -Let's run through an example of how to publish the above upgradeable contract to a resource account and upgrade it. - -1. Publish the module to a resource account -2. Run the `upgradeable_function` view function and see what it returns -3. Upgrade the module using the json output from the `aptos move build-publish-package` command -4. Run the `upgradeable_function` view function again to see the new return value - -First make sure you have a default profile initialized to devnet. - -```shell -aptos init --profile default -``` - -Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `default` in our commands, it will automatically use this profile. - -Navigate to the `move-examples/upgrade_resource_contract` directory. - -### Publish the module - -```shell -aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --seed '' --named-addresses owner=default -``` - -The `--address-name` flag denotes that the resource address created from the resource account we make will be supplied as the `upgrade_resource_contract` address in our module. Since we declared it as the module address with `module upgrade_resource_contract::upgrader { ... }` at the very top of our contract, this is where our contract will be deployed. - -When you run this command, it will ask you something like this: - -``` -Do you want to publish this package under the resource account's address be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? [yes/no] > -``` - -Say yes and copy that address to your clipboard. That's our resource account address where the contract is deployed. - -Now you can run the view function! - -### Run the view function - -```shell -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function -``` - -Remember to replace `RESOURCE_ACCOUNT_ADDRESS` with the resource account address you deployed your module to; it is different from the one posted above, so this will specifically only work for *your* contract. - -It should output: -```json -Result: [ - 9000 -] -``` - -### Change the view function - -Now let's change the value returned in the view function from `9000` to `9001` so we can see that we've upgraded the contract: - -```rust -#[view] -public fun upgradeable_function(): u64 { - 9001 -} -``` - -Save that file, and then use the `build-publish-package` command to get the bytecode output in JSON format. - -### Get the bytecode for the module - -```shell -aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgrade_resource_contract=RESOURCE_ACCOUNT_ADDRESS,owner=default -``` - -Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. - -The hex values in this JSON file are arguments that we'd normally use to pass into the `0x1::code::publish_package_txn` function, but since we made our own `upgrade_contract` function that wraps it, we need to change the function call value to something else. - -Your JSON should look something like the below output, just with expanded `value` fields (truncated here for simplicity's sake): - -```json -{ - "function_id": "0x1::code::publish_package_txn", - "type_args": [], - "args": [ - { - "type": "hex", - "value": "0x2155...6200" - }, - { - "type": "hex", - "value": [ - "0xa11c...0000" - ] - } - ] -} -``` - -Change the `function_id` value in the JSON file to match your contract's upgrade function contract, with your resource account address filled in: - -``` -"function_id": "RESOURCE_ACCOUNT_ADDRESS::upgrader::upgrade_contract", -``` - -Save this file so we can use it to run an entry function with JSON parameters. - -### Run the upgrade_contract function - -```shell -aptos move run --json-file upgrade_contract.json -``` - -Confirm yes to publish the upgraded module where the view function will return 9001 instead of 9000. - -### Run the upgraded view function - -```shell -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function -``` - -You should get: - -```json -Result: [ - 9001 -] -``` - -Now you know how to publish an upgradeable module to a resource account! - -## Creating and funding a resource account - -Another common usage is to create and fund a resource account, in case the account needs access to functions that need access to `Coin`: - -```rust -// resource_account.move -public entry fun create_resource_account_and_fund( - origin: &signer, - seed: vector, - optional_auth_key: vector, - fund_amount: u64, -) acquires Container { - let (resource, resource_signer_cap) = account::create_resource_account(origin, seed); - coin::register(&resource); - coin::transfer(origin, signer::address_of(&resource), fund_amount); - rotate_account_authentication_key_and_store_capability( - origin, - resource, - resource_signer_cap, - optional_auth_key, - ); -} -``` - -## Acquiring a SignerCapability later - -Say you create a resource account and rotate its authentication key to your account's authentication key. You'd just need to sign for the account and call `retrieve_resource_account_cap` in order to get the `SignerCapability` and store it somewhere: - -```rust -struct MySignerCapability has key { - resource_signer_cap: SignerCapability, -} - -public entry fun retrieve_cap(resource_signer: &signer, source_addr: address): acquires MySignerCapability { - let resource_signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, source_addr); - move_to(resource_signer, MySignerCapability { - resource_signer_cap, - }); -} -``` - -Call the function, but change the sender account to appear as the resource account with the CLI flag `--sender-account`. If the source address is the `default` profile: - -```shell -aptos move run --function-id MODULE_ADDRESS::MODULE_NAME::retrieve_cap --args address:default --sender-account RESOURCE_ADDRESS_HERE --profile default -``` - -## How is the address for a resource account derived? - -When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address plus an optional byte vector `seed`. If you want to know the resource address generated by an account and a given seed, you can call the `create_resource_address` function in `account.move`: -```rust -/// This is a helper function to compute resource addresses. Computation of the address -/// involves the use of a cryptographic hash operation and should be use thoughtfully. -public fun create_resource_address(source: &address, seed: vector): address { - let bytes = bcs::to_bytes(source); - vector::append(&mut bytes, seed); - vector::push_back(&mut bytes, DERIVE_RESOURCE_ACCOUNT_SCHEME); - from_bcs::to_address(hash::sha3_256(bytes)) -} -``` - -## What's a SignerCapability? - -A SignerCapability is a very simple resource, intended to be an abstract representation of the ability to sign for an account. It doesn't actually *do* anything special, but its existence somewhere implies that if you have access to it, you either created it or received access to it very intentionally. - -It contains a single field called `account` the matches the address it's intended to generate a signature for: - -```rust -struct SignerCapability has drop, store { - account: address -} -``` - -Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. diff --git a/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md new file mode 100644 index 0000000000000..52b3efaab72a6 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md @@ -0,0 +1,342 @@ +--- +title: "Utilizing resource accounts" +id: "utilizing-resource-accounts" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Utilizing Resource Accounts + +In this guide we're going to show you how you can use resource accounts to automate smart contracts and manage resources programmatically. If you're looking for an introduction to resource accounts, head to the [conceptual understanding](../resource-accounts/understanding-resource-accounts) section. + +There are two distinct ways to manage a resource account: + +1. The authentication key is rotated to a separate account that can control it manually by signing for it +2. The authentication key is rotated to **0x0** and is controlled programmatically with a [**SignerCapability**](./understanding-resource-accounts#whats-a-signercapability) + +:::info +The `create_resource_account(...)` functions in [`account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [`resource_account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move) are different, with the `account.move` version creating a programmatically controlled resource account and the `resource_account.move` version creating a manually controlled resource account. +::: + +If you'd like to better understand how the creation functions work, check out [how are resource accounts created.](../resource-accounts/understanding-resource-accounts#how-are-resource-accounts-created) + +## Using a SignerCapability + +The [**SignerCapability**](./understanding-resource-accounts#whats-a-signercapability) resource is the most crucial part of how resource accounts work. + +When a SignerCapability is created and later retrieved, the authentication key of the resource account it was created for is rotated to **0x0**, which gives the [Move VM](../../reference/glossary/#move-virtual-machine-mvm) the capability to generate the resource account's signer from a SignerCapability. You can store this SignerCapability and retrieve it later to create an authorized signer for the resource account. + +Here is a very basic example that demonstrates how you'd use a SignerCapability in a Move contract: + +```rust +// Define a resource we can store the SignerCapability at +struct SignerCap has key { + signer_cap: SignerCapability, +} + +public entry fun store_signer_capability(creator: &signer) { + // We move `SignerCap` to an account's resources. We can even move it to the resource account itself: + let (resource_signer, signer_cap) = account::create_resource_account(creator, b"seed bytes"); + move_to(resource_signer, SignerCap { + signer_cap, + }); +} + +// Generate the resource account's signer with the SignerCapability +public entry fun sign_with_resource_account(creator: &signer) acquires SignerCap { + let resource_address = account::create_resource_address(signer::address_of(creator), b"seed bytes"); + let signer_cap = borrow_global(resource_account_address); + let resource_signer = account::create_signer_with_capability(signer_cap); + + // Here we'd do something with the resource_signer that we can only do with its `signer` primitive +} +``` +Utilizing a resource account in this way is the fundamental process for automating the generation and retrieval of resources on-chain. + +If you're wondering how the **SignerCapability** permission model works, head over to [what's stopping someone from using my SignerCapability?](./understanding-resource-accounts#whats-stopping-someone-from-using-my-signercapability) + +## Retrieving a SignerCapability + +Say you create a resource account with one of the `resource_account.move` functions. The `SignerCapability` exists for the account but you need to retrieve it- you can do this by calling `retrieve_resource_account_cap`. + +Here's an example of how you could achieve this: + +```rust title="Retrieve and store a SignerCapability" +struct SignerCap has key { + signer_cap: SignerCapability, +} + +// `source_addr` is the address of the resource account creator +public entry fun retrieve_cap(resource_signer: &signer, source_addr: address) acquires SignerCap { + // Retrieve the SignerCapability + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, source_addr); + // Move it into the resource signer's account resources for use later + move_to(resource_signer, SignerCap { + signer_cap, + }); +} +``` +:::warning +Make sure to store a retrieved `SignerCapability` somewhere because the `retrieve_resource_account_cap` function can only be called once, since the `SignerCapability` returned is ephemeral and will be dropped if not stored. + +Without access to a `SignerCapability`, there is no way to generate a signature for the account, effectively locking you out of it forever. +::: + +## Publishing a module to a resource account + +One of the most common usages of resource accounts is publishing a module with them. This function in `resource_account.move` is called by a user account to create a resource account and publish a module with it. + +```rust title="Helper function in resource_account.move to publish a package to a resource account" +public entry fun create_resource_account_and_publish_package( + origin: &signer, + seed: vector, + metadata_serialized: vector, + code: vector>, +) acquires Container { + let (resource, signer_cap) = account::create_resource_account(origin, seed); + aptos_framework::code::publish_package_txn(&resource, metadata_serialized, code); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + signer_cap, + ZERO_AUTH_KEY, + ); +} +``` + +:::warning Immutable Contracts +By default, publishing a module to a resource account will result in an immutable contract. This is because the **SignerCapability** is retrieved and dropped during the publishing process. +::: + +If you'd like to publish an upgradeable module to a resource account, see the section below. + +## Publishing an upgradeable module with a resource account + +Publishing an upgradeable module with a resource account involves a few steps to setup the contract to prepare it for publication and then the actual process of publishing and upgrading. In this section we'll explain how it works first and then how to do it after. + +### How it works + +The key to making an upgradeable contract with a resource account is to use Aptos Move's **init_module** function. The **init_module** function is a unique function that will **only** run the first time a contract is published. + +It's always a private function with a single argument (the module publisher, in the form of `&signer`) with no return value. + +```rust title="init_module function signature" +fun init_module(publisher: &signer) { + // ... +} +``` + +We can use this function to retrieve the SignerCapability and store it somewhere for use later: + +```rust title="Using init_module to store a publishing resource account's SignerCapability" +module upgradeable_resource_contract::package_manager { + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::resource_account; + use aptos_std::code; + use std::signer; + + /// You are not authorized to upgrade this module. + const ENOT_AUTHORIZED: u64 = 0; + + // Declare our SignerCap struct to store our resource account's SignerCapability. + struct SignerCap has key { + signer_cap: SignerCapability, + } + + fun init_module(resource_signer: &signer) { + // Note that we must deploy the module with `deployer` as a named address, otherwise the contract can't find the resource account's owner. + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @deployer); + + // We move the SignerCap to the resource account. + // Note that this means the SignerCap resource is stored at the same address that the module is. + move_to(resource_signer, SignerCap { + signer_cap, + }); + } + + // NOTE: This function is integral to making the contract upgradeable. Without it, the SignerCapability is stored, but + // there is no way to actually use it to upgrade the module. + public entry fun upgrade_module( + deployer: &signer, + package_metadata: vector, + code: vector>, + ) acquires SignerCap { + // NOTE: If we leave this line out, anyone can upgrade the contract and potentially hijack its functionality. + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + let signer_cap = &borrow_global(@upgrade_resource_contract).signer_cap; + let resource_signer = account::create_signer_with_capability(signer_cap); + code::publish_package_txn(&resource_signer, package_metadata, code); + } +} +``` + +There are several things going on here to take note of: + +1. We retrieve and store the SignerCapability in the `SignerCap` struct. +2. We add an `upgrade_module(...)` function that acts as an interface to the package publishing function. It allows the **deployer** of the module to upgrade it despite not being the direct owner of the module. +3. The **deployer** is the developer's account- the one that calls `create_resource_account_and_publish_package(...)` and owns the resource account but not the module itself. It is a named address here, signified with `@deployer`, so we must specify it as a named address upon publication. +4. We gate access to the `upgrade_module` function by asserting that the signer is the original `@deployer`. + +Publishing this module from the Aptos CLI would look like this: + +```shell +aptos move create-resource-account-and-publish-package \ + --address-name upgrade_resource_contract \ + --named-addresses owner=CONTRACT_DEPLOYER \ + --profile CONTRACT_DEPLOYER +``` + +Where `CONTRACT_DEPLOYER` is the profile. Read more about [Aptos CLI profiles here.](../../tools/aptos-cli/use-cli/use-aptos-cli#creating-other-profiles) + +### Step-by-step guide + +Let's run through an example of how to publish the above upgradeable contract to a resource account and upgrade it. + +1. Publish the module to a resource account +2. Run the `upgradeable_function` view function and see what it returns +3. Upgrade the module using the json output from the `aptos move build-publish-package` command +4. Run the `upgradeable_function` view function again to see the new return value + +First make sure you have a default profile initialized to devnet. + +```shell +aptos init --profile default +``` + +Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `default` in our commands, it will automatically use this profile. + +Navigate to the `move-examples/upgrade_resource_contract` directory. + +### Publish the module + +```shell +aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --seed '' --named-addresses owner=default +``` + +The `--address-name` flag denotes that the resource address created from the resource account we make will be supplied as the `upgrade_resource_contract` address in our module. Since we declared it as the module address with `module upgrade_resource_contract::upgrader { ... }` at the very top of our contract, this is where our contract will be deployed. + +When you run this command, it will ask you something like this: + +``` +Do you want to publish this package under the resource account's address be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? [yes/no] > +``` + +Say yes and copy that address to your clipboard. That's our resource account address where the contract is deployed. + +Now you can run the view function! + +### Run the view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +``` + +Remember to replace `RESOURCE_ACCOUNT_ADDRESS` with the resource account address you deployed your module to; it is different from the one posted above, so this will specifically only work for *your* contract. + +It should output: +```json +Result: [ + 9000 +] +``` + +### Change the view function + +Now let's change the value returned in the view function from `9000` to `9001` so we can see that we've upgraded the contract: + +```rust +#[view] +public fun upgradeable_function(): u64 { + 9001 +} +``` + +Save that file, and then use the `build-publish-package` command to get the bytecode output in JSON format. + +### Get the bytecode for the module + +```shell +aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgrade_resource_contract=RESOURCE_ACCOUNT_ADDRESS,owner=default +``` + +Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. + +The hex values in this JSON file are arguments that we'd normally use to pass into the `0x1::code::publish_package_txn` function, but since we made our own `upgrade_contract` function that wraps it, we need to change the function call value to something else. + +Your JSON should look something like the below output, just with expanded `value` fields (truncated here for simplicity's sake): + +```json +{ + "function_id": "0x1::code::publish_package_txn", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x2155...6200" + }, + { + "type": "hex", + "value": [ + "0xa11c...0000" + ] + } + ] +} +``` + +Change the `function_id` value in the JSON file to match your contract's upgrade function contract, with your resource account address filled in: + +``` +"function_id": "RESOURCE_ACCOUNT_ADDRESS::upgrader::upgrade_contract", +``` + +Save this file so we can use it to run an entry function with JSON parameters. + +### Run the upgrade_contract function + +```shell +aptos move run --json-file upgrade_contract.json +``` + +Confirm yes to publish the upgraded module where the view function will return 9001 instead of 9000. + +### Run the upgraded view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +``` + +You should get: + +```json +Result: [ + 9001 +] +``` + +Now you know how to publish an upgradeable module to a resource account! + +## Creating and funding a resource account + +Another common usage is to create and fund a resource account, in case the account needs access to functions that need access to `Coin`: + +```rust +// resource_account.move +public entry fun create_resource_account_and_fund( + origin: &signer, + seed: vector, + optional_auth_key: vector, + fund_amount: u64, +) acquires Container { + let (resource, signer_cap) = account::create_resource_account(origin, seed); + coin::register(&resource); + coin::transfer(origin, signer::address_of(&resource), fund_amount); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + signer_cap, + optional_auth_key, + ); +} +``` diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index 63bfe9672eb30..d499196fce861 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -366,7 +366,8 @@ const sidebars = { collapsible: true, collapsed: true, items: [ - "guides/resource-accounts/using-resource-accounts", + "guides/resource-accounts/utilizing-resource-accounts", + "guides/resource-accounts/understanding-resource-accounts", ], }, ], From 038ded8c82d93eba60034cfdf2cee0bae0f90743 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:12:15 -0700 Subject: [PATCH 06/10] Add tabs import to understanding page and fix sidebars.js --- .../understanding-resource-accounts.md | 3 +++ developer-docs-site/sidebars.js | 13 ------------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md index 096625b7d6603..1ceb396239bea 100644 --- a/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md +++ b/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md @@ -3,6 +3,9 @@ title: "Conceptual understanding" id: "understanding-resource-accounts" --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Conceptual understanding ## What is a resource account? diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index d499196fce861..4a99a3a5362b9 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -336,11 +336,6 @@ const sidebars = { }, { type: "category", -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= ->>>>>>> 2cc005c6a5 (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) label: "Examples", collapsible: true, collapsed: true, @@ -351,12 +346,6 @@ const sidebars = { slug: "/category/examples", keywords: ["examples"], }, -<<<<<<< HEAD - items: ["guides/account-management/key-rotation"], - }, - { - type: "category", -======= items: [ "guides/account-management/key-rotation", { @@ -374,8 +363,6 @@ const sidebars = { }, { type: "category", ->>>>>>> 55c0f3ab0c (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) ->>>>>>> 2cc005c6a5 (Updated inaccurate wording, explained the difference between account.move and resource_account.move, and moved the structure around. Also added auth key rotation examples from #9130 to refer to.) label: "Build E2E Dapp with Aptos", link: { type: "doc", id: "tutorials/build-e2e-dapp/index" }, collapsible: true, From 9aee8608df1df91ca29d754fc648c4a7cda0a30f Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:32:05 -0700 Subject: [PATCH 07/10] Finishing the resource account tutorial with kevin's package manager, explanation of it and features, and separating the guide into 3 sections. Updated unit test. --- aptos-move/e2e-move-tests/src/tests/mod.rs | 2 +- ...> upgradeable_resource_account_package.rs} | 47 ++-- .../Move.toml | 12 + .../sources/basic_contract.move | 33 +++ .../sources/package_manager.move | 17 +- .../upgrade_contract.json | 0 .../upgradeable_resource_contract/Move.toml | 12 - .../sources/basic_contract.move | 6 - ...source-accounts.md => common-questions.md} | 8 +- .../docs/guides/resource-accounts/index.md | 5 +- .../publishing-an-upgradeable-module.md | 192 ++++++++++++++++ .../utilizing-resource-accounts.md | 210 ++---------------- developer-docs-site/sidebars.js | 3 +- 13 files changed, 313 insertions(+), 234 deletions(-) rename aptos-move/e2e-move-tests/src/tests/{upgradeable_resource_contract.rs => upgradeable_resource_account_package.rs} (79%) create mode 100644 aptos-move/move-examples/upgradeable_resource_account_package/Move.toml create mode 100644 aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move rename aptos-move/move-examples/{upgradeable_resource_contract => upgradeable_resource_account_package}/sources/package_manager.move (82%) rename aptos-move/move-examples/{upgradeable_resource_contract => upgradeable_resource_account_package}/upgrade_contract.json (100%) delete mode 100644 aptos-move/move-examples/upgradeable_resource_contract/Move.toml delete mode 100644 aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move rename developer-docs-site/docs/guides/resource-accounts/{understanding-resource-accounts.md => common-questions.md} (96%) create mode 100644 developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index bd22dafdb7df4..5f18309e63d2c 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -39,6 +39,6 @@ mod token_event_store; mod token_objects; mod transaction_fee; mod type_too_large; -mod upgradeable_resource_contract; +mod upgradeable_resource_account_package; mod vector_numeric_address; mod vote; diff --git a/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs similarity index 79% rename from aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs rename to aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs index b60ba07db6ee6..b252c53e2cafc 100644 --- a/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_contract.rs +++ b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs @@ -5,6 +5,13 @@ use crate::{assert_success, tests::common, MoveHarness}; use aptos_package_builder::PackageBuilder; use aptos_types::account_address::{create_resource_address, AccountAddress}; use aptos_framework::natives::code::{PackageMetadata, UpgradePolicy}; +use move_core_types::parser::parse_struct_tag; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Eq, PartialEq)] +struct SomeResource { + value: u64, +} fn custom_build_helper( deployer_address: AccountAddress, @@ -12,17 +19,17 @@ fn custom_build_helper( package_manager_code: &String, basic_contract_code: &String, ) -> (PackageMetadata, Vec>) { - // add the named addresses for `deployer` and `upgradeable_resource_contract` + // add the named addresses for `deployer` and `upgradeable_resource_account_package` let mut build_options = aptos_framework::BuildOptions::default(); build_options .named_addresses .insert("deployer".to_string(), deployer_address); build_options .named_addresses - .insert("upgradeable_resource_contract".to_string(), resource_address); + .insert("upgradeable_resource_account_package".to_string(), resource_address); let mut package_builder = - PackageBuilder::new("Upgradeable Resource Account Contract") + PackageBuilder::new("Upgradeable Module With Resource Account") .with_policy(UpgradePolicy::compat()); package_builder.add_source("package_manager", &package_manager_code); package_builder.add_source("basic_contract", &basic_contract_code); @@ -49,23 +56,14 @@ fn code_upgrading_using_resource_account() { let deployer = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); let resource_address = create_resource_address(*deployer.address(), &[]); - // add the named addresses for `deployer` and `upgradeable_resource_contract` - let mut build_options = aptos_framework::BuildOptions::default(); - build_options - .named_addresses - .insert("deployer".to_string(), *deployer.address()); - build_options - .named_addresses - .insert("upgradeable_resource_contract".to_string(), resource_address); - // get contract code from file let package_manager_code = std::fs::read_to_string( - &common::test_dir_path("../../../move-examples/upgradeable_resource_contract/sources/package_manager.move") + &common::test_dir_path("../../../move-examples/upgradeable_resource_account_package/sources/package_manager.move") ).unwrap(); let basic_contract_code = std::fs::read_to_string( - &common::test_dir_path("../../../move-examples/upgradeable_resource_contract/sources/basic_contract.move") + &common::test_dir_path("../../../move-examples/upgradeable_resource_account_package/sources/basic_contract.move") ).unwrap(); let (metadata, code) = custom_build_helper( @@ -132,4 +130,25 @@ fn code_upgrading_using_resource_account() { ).unwrap().pop().unwrap(); let result = bcs::from_bytes::(&bcs_result).unwrap(); assert_eq!(AFTER_VALUE, result, "assert view function result {} == {}", result, AFTER_VALUE); + + // test the `move_to_rseource_account(...)` function by moving SomeResource into the resource + // account + assert_success!(h.run_entry_function( + &deployer, + str::parse(&format!( + "0x{}::basic_contract::move_to_resource_account", + resource_address + )).unwrap(), + vec![], + vec![], + )); + + let some_resource = parse_struct_tag(&format!( + "0x{}::basic_contract::SomeResource", + resource_address + )).unwrap(); + let some_resource_value = h + .read_resource::(&resource_address, some_resource) + .unwrap(); + assert_eq!(some_resource_value.value, 42, "assert SomeResource.value == 42"); } diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml b/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml new file mode 100644 index 0000000000000..f76f0632c5e82 --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml @@ -0,0 +1,12 @@ +[package] +name = "Upgradeable Module With Resource Account" +version = "0.0.0" +upgrade_policy = "compatible" + +[addresses] +aptos_framework = "0x1" +deployer = "_" +upgradeable_resource_account_package = "_" + +[dependencies] +AptosFramework = { local = "../../framework/aptos-framework" } diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move b/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move new file mode 100644 index 0000000000000..8e2c78acaca2b --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move @@ -0,0 +1,33 @@ +module upgradeable_resource_account_package::basic_contract { + use upgradeable_resource_account_package::package_manager; + use std::error; + use std::signer; + + struct SomeResource has key { + value: u64, + } + + /// You are not authorized to perform this action. + const ENOT_AUTHORIZED: u64 = 0; + + #[view] + public fun upgradeable_function(): u64 { + 9000 + } + + // An example of doing something with the resource account that requires its signer + public entry fun move_to_resource_account(deployer: &signer) { + // Only the deployer can call this function. + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + + // Do something with the resource account's signer + // For example, a simple `move_to` call + let resource_signer = package_manager::get_signer(); + move_to( + &resource_signer, + SomeResource { + value: 42, + } + ); + } +} diff --git a/aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move b/aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move similarity index 82% rename from aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move rename to aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move index e0e65ef3fd797..abbcd0450f739 100644 --- a/aptos-move/move-examples/upgradeable_resource_contract/sources/package_manager.move +++ b/aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move @@ -1,4 +1,4 @@ -module upgradeable_resource_contract::package_manager { +module upgradeable_resource_account_package::package_manager { use aptos_framework::account::{Self, SignerCapability}; use aptos_framework::resource_account; use aptos_std::smart_table::{Self, SmartTable}; @@ -6,6 +6,7 @@ module upgradeable_resource_contract::package_manager { use aptos_std::code; use std::error; use std::signer; + friend upgradeable_resource_account_package::basic_contract; /// The signer is not authorized to deploy this module. const ENOT_AUTHORIZED: u64 = 0; @@ -39,27 +40,27 @@ module upgradeable_resource_contract::package_manager { /// Can be called by friended modules to obtain the resource account signer. public(friend) fun get_signer(): signer acquires PermissionConfig { - let signer_cap = &borrow_global(@upgradeable_resource_contract).signer_cap; + let signer_cap = &borrow_global(@upgradeable_resource_account_package).signer_cap; account::create_signer_with_capability(signer_cap) } /// Can be called by friended modules to keep track of a system address. - public(friend) fun add_address(name: String, object: address) acquires PermissionConfig { - let addresses = &mut borrow_global_mut(@upgradeable_resource_contract).addresses; + public(friend) fun add_named_address(name: String, object: address) acquires PermissionConfig { + let addresses = &mut borrow_global_mut(@upgradeable_resource_account_package).addresses; smart_table::add(addresses, name, object); } - public fun address_exists(name: String): bool acquires PermissionConfig { + public fun named_address_exists(name: String): bool acquires PermissionConfig { smart_table::contains(&safe_permission_config().addresses, name) } - public fun get_address(name: String): address acquires PermissionConfig { - let addresses = &borrow_global(@upgradeable_resource_contract).addresses; + public fun get_named_address(name: String): address acquires PermissionConfig { + let addresses = &borrow_global(@upgradeable_resource_account_package).addresses; *smart_table::borrow(addresses, name) } inline fun safe_permission_config(): &PermissionConfig acquires PermissionConfig { - borrow_global(@upgradeable_resource_contract) + borrow_global(@upgradeable_resource_account_package) } #[test_only] diff --git a/aptos-move/move-examples/upgradeable_resource_contract/upgrade_contract.json b/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json similarity index 100% rename from aptos-move/move-examples/upgradeable_resource_contract/upgrade_contract.json rename to aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json diff --git a/aptos-move/move-examples/upgradeable_resource_contract/Move.toml b/aptos-move/move-examples/upgradeable_resource_contract/Move.toml deleted file mode 100644 index 09c2e38fc268a..0000000000000 --- a/aptos-move/move-examples/upgradeable_resource_contract/Move.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "Upgradeable Resource Account Contract" -version = "0.0.0" -upgrade_policy = "compatible" - -[addresses] -aptos_framework = "0x1" -deployer = "_" -upgradeable_resource_contract = "_" - -[dependencies] -AptosFramework = { local = "../../framework/aptos-framework" } \ No newline at end of file diff --git a/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move b/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move deleted file mode 100644 index 5aa03383b3406..0000000000000 --- a/aptos-move/move-examples/upgradeable_resource_contract/sources/basic_contract.move +++ /dev/null @@ -1,6 +0,0 @@ -module upgradeable_resource_contract::basic_contract { - #[view] - public fun upgradeable_function(): u64 { - 9000 - } -} diff --git a/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/common-questions.md similarity index 96% rename from developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md rename to developer-docs-site/docs/guides/resource-accounts/common-questions.md index 1ceb396239bea..5c2e7ee2c79d0 100644 --- a/developer-docs-site/docs/guides/resource-accounts/understanding-resource-accounts.md +++ b/developer-docs-site/docs/guides/resource-accounts/common-questions.md @@ -1,12 +1,12 @@ --- -title: "Conceptual understanding" -id: "understanding-resource-accounts" +title: "Common questions" +id: "common-questions" --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Conceptual understanding +# Common questions ## What is a resource account? @@ -102,7 +102,7 @@ You might be wondering "*Why does this work? Isn't it dangerous to be able to cr Move's [privileged struct operations](../../move/book/structs-and-resources#privileged-struct-operations) require that creating structs and accessing their inner fields can only occur from within the module that defines the struct. This means that unless the developer provides a public accessor function to a stored `SignerCapability`, there is no way for another account to gain access to it. :::warning -Be mindful of properly gating access to a function that uses a `SignerCapability` and be extra careful when returning it from a function, since this gives the caller unrestricted access to control the account. +Be mindful of properly gating access to a function that uses a `SignerCapability` and be extra careful when returning a `signer` from a public function, since this gives the caller unrestricted access to control the account. ::: A good rule of thumb is to by default set all functions that return a `SignerCapability` or a `signer` to internal private functions unless you've very carefully thought about the implications. diff --git a/developer-docs-site/docs/guides/resource-accounts/index.md b/developer-docs-site/docs/guides/resource-accounts/index.md index 205699c3b531b..a90d3cfe1e7b1 100644 --- a/developer-docs-site/docs/guides/resource-accounts/index.md +++ b/developer-docs-site/docs/guides/resource-accounts/index.md @@ -2,7 +2,8 @@ title: "Resource Accounts" --- -# Examples of the various ways to use resource accounts +# Resource Accounts - ### [Utilizing resource accounts](./utilizing-resource-accounts) -- ### [Understanding resource accounts](./understanding-resource-accounts) +- ### [Publishing an upgradeable module](./publishing-an-upgradeable-module) +- ### [Common questions](./common-questions) diff --git a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md new file mode 100644 index 0000000000000..d97a9a4b1b878 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md @@ -0,0 +1,192 @@ +--- +title: "Publishing an upgradeable module" +id: "publishing-an-upgradeable-module" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Publishing an upgradeable module + +The objective of this tutorial is to show a concrete example of how to publish an upgradeable module with a resource account. + +In order to follow along with this tutorial, make sure you have: + +* The [Aptos CLI](../../tools/aptos-cli) +* The [Aptos core repository.](https:/github.com/aptos-labs/aptos-core) The modules we'll use are located in the `aptos-move/move-examples` section of the repository. + +## Overview of the publishing process + +Each of the following steps will be explained in detail in their corresponding sections: + +1. Publish the module with a resource account +2. Run the `upgradeable_function` view function and see what it returns +3. Change the view function in the contract so we can observe a tangible change post-publish +4. Get the module metadata and bytecode from the `aptos move build-publish-package` command +5. Run the `publish_package` function to upgrade the module +6. Run the view function again to observe the new return value + +First make sure you have a `deployer` profile initialized to devnet. + +```shell +aptos init --profile `deployer` +``` + +Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `deployer` in our commands, it will automatically use this profile. + +```shell title="Navigate to your local directory" +cd ~/aptos-core/aptos-move/move-examples/upgradeable_resource_account_package +``` + +### 1. Publish the module + +```shell +aptos move create-resource-account-and-publish-package \\ + --address-name upgradeable_resource_account_package --seed '' \\ + --named-addresses owner=deployer +``` + +The `--address-name` flag marks the following string as named address the resource account's address will appear as in the contract. + +That is, the resource account created from this command will correspond to the `@upgradeable_resource_account_package` address in our module. + +When you run this command, it will ask you something like this: + +``` +Do you want to publish this package under the resource account's address +be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? +[yes/no] > +``` + +Enter yes and copy that address down somewhere. That's our resource account address where the contract is deployed. Now you can run the view function! + +### 2. Run the view function + +Replace `RESOURCE_ACCOUNT_ADDRESS`` with the address you got from step #1 and run the following command: + +```shell title="View the value returned from upgradeable_function()" +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +``` + +It should output: +```json +Result: [ + 9000 +] +``` + +### 3. Change the view function + +Now let's change the value returned in the view function from `9000` to `9001` so we can observe a difference in the upgraded contract: + +```rust +#[view] +public fun upgradeable_function(): u64 { + 9001 +} +``` + +Save that file, and then follow step #4 to get the package metadata and bytecode in JSON format. + +### 4. Get the new bytecode for the module + +```shell +aptos move build-publish-payload --json-output-file upgrade_contract.json \\ + --named-addresses upgradeable_resource_account_package=RESOURCE_ACCOUNT_ADDRESS,owner=deployer +``` + +Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. + +Since we made our own `upgrade_contract` function that wraps the `0x1::code::publish_package_txn`, we need to change the function call value to our publish package function. + +After editing the JSON output, your file should look something like below, except you'll have much longer expanded `value` fields (truncated here for simplicity's sake): + +```json +{ + "function_id": "RESOURCE_ACCOUNT_ADDRESS::package_manager::publish_package", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x2155...6200" + }, + { + "type": "hex", + "value": [ + "0xa11c...0000" + ] + } + ] +} +``` + +Make sure to change the `RESOURCE_ACCOUNT_ADDRESS` to your specific resource account address. + +### 5. Run the upgrade_contract function + +```shell +aptos move run --json-file upgrade_contract.json +``` + +Confirm yes to publish the upgraded module. + +### 6. Run the upgraded view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function +``` + +You should get: + +```json +Result: [ + 9001 +] +``` + +Now you know how to publish an upgradeable module with a resource account! + +## Extra features in the package manager module + +You may have noticed there are other features in the `package_manager.move` module. These are helper functions to manage retrieving the `signer` for a resource account used to publish a package module and additionally track addresses generated from the contract in a `SmartTable`. + +The `get_signer()` function utilizes the friend keyword to gate access to the signer. +```rust title="The get_signer() function can only be called by friends or other functions in package_manager.move" +public(friend) fun get_signer(): signer acquires PermissionConfig { + let signer_cap = &borrow_global(@upgradeable_resource_account_package).signer_cap; + account::create_signer_with_capability(signer_cap) +} +``` + +If you wanted to give another one of your modules access to this function this function, you'd simply declare the module as a friend at the top of your module, and then call it in that module wherever you need: + +```rust +// package_manager.move +module upgradeable_resource_account_package::package_manager { + friend upgradeable_resource_account_package::basic_contract; + // ... +} + +// basic_contract.move +module upgradeable_resource_account_package::basic_contract { + use upgradeable_resource_account_package::package_manager; + // ... + public fun move_to_resource_account(deployer: &signer) { + // Only the deployer can call this function. + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + + // Do something with the resource account's signer + // For example, a simple `move_to` call + let resource_signer = package_manager::get_signer(); + move_to( + &resource_signer, + SomeResource { + value: 42, + } + ); + } +} +``` +Read more about [friend function declarations here.](../../move/book/friends/#friend-declaration) + +Use the package manager contract as needed in your own packages. Keep in mind that you must deploy the `package_manager.move` module with your package in order to correctly utilize the module. diff --git a/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md index 52b3efaab72a6..3f928e1d06099 100644 --- a/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md +++ b/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md @@ -6,24 +6,24 @@ id: "utilizing-resource-accounts" import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Utilizing Resource Accounts +# General usage -In this guide we're going to show you how you can use resource accounts to automate smart contracts and manage resources programmatically. If you're looking for an introduction to resource accounts, head to the [conceptual understanding](../resource-accounts/understanding-resource-accounts) section. +In this section we're going to explain the mechanisms behind general usage of resource accounts and how we can use them to automate smart contracts and manage resources programmatically. There are two distinct ways to manage a resource account: 1. The authentication key is rotated to a separate account that can control it manually by signing for it -2. The authentication key is rotated to **0x0** and is controlled programmatically with a [**SignerCapability**](./understanding-resource-accounts#whats-a-signercapability) +2. The authentication key is rotated to **0x0** and is controlled programmatically with a [**SignerCapability**](./common-questions#whats-a-signercapability) :::info The `create_resource_account(...)` functions in [`account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [`resource_account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move) are different, with the `account.move` version creating a programmatically controlled resource account and the `resource_account.move` version creating a manually controlled resource account. ::: -If you'd like to better understand how the creation functions work, check out [how are resource accounts created.](../resource-accounts/understanding-resource-accounts#how-are-resource-accounts-created) +If you'd like to better understand how the creation functions work, check out [how are resource accounts created.](../resource-accounts/common-questions#how-are-resource-accounts-created) ## Using a SignerCapability -The [**SignerCapability**](./understanding-resource-accounts#whats-a-signercapability) resource is the most crucial part of how resource accounts work. +The [**SignerCapability**](./common-questions#whats-a-signercapability) resource is the most crucial part of how resource accounts work. When a SignerCapability is created and later retrieved, the authentication key of the resource account it was created for is rotated to **0x0**, which gives the [Move VM](../../reference/glossary/#move-virtual-machine-mvm) the capability to generate the resource account's signer from a SignerCapability. You can store this SignerCapability and retrieve it later to create an authorized signer for the resource account. @@ -54,7 +54,7 @@ public entry fun sign_with_resource_account(creator: &signer) acquires SignerCap ``` Utilizing a resource account in this way is the fundamental process for automating the generation and retrieval of resources on-chain. -If you're wondering how the **SignerCapability** permission model works, head over to [what's stopping someone from using my SignerCapability?](./understanding-resource-accounts#whats-stopping-someone-from-using-my-signercapability) +If you're wondering how the **SignerCapability** permission model works, head over to [what's stopping someone from using my SignerCapability?](./common-questions#whats-stopping-someone-from-using-my-signercapability) ## Retrieving a SignerCapability @@ -83,11 +83,13 @@ Make sure to store a retrieved `SignerCapability` somewhere because the `retriev Without access to a `SignerCapability`, there is no way to generate a signature for the account, effectively locking you out of it forever. ::: -## Publishing a module to a resource account +## Publishing modules with resource accounts + +### Publishing an immutable module with a resource account One of the most common usages of resource accounts is publishing a module with them. This function in `resource_account.move` is called by a user account to create a resource account and publish a module with it. -```rust title="Helper function in resource_account.move to publish a package to a resource account" +```rust title="Helper function in resource_account.move to publish a package with a resource account" public entry fun create_resource_account_and_publish_package( origin: &signer, seed: vector, @@ -106,17 +108,15 @@ public entry fun create_resource_account_and_publish_package( ``` :::warning Immutable Contracts -By default, publishing a module to a resource account will result in an immutable contract. This is because the **SignerCapability** is retrieved and dropped during the publishing process. +By default, publishing a module with a resource account will result in an immutable module. This is because the **SignerCapability** is retrieved and dropped during the publishing process. ::: -If you'd like to publish an upgradeable module to a resource account, see the section below. +If you'd like to publish an upgradeable module with a resource account, see the section below. -## Publishing an upgradeable module with a resource account +### Publishing an upgradeable module with a resource account Publishing an upgradeable module with a resource account involves a few steps to setup the contract to prepare it for publication and then the actual process of publishing and upgrading. In this section we'll explain how it works first and then how to do it after. -### How it works - The key to making an upgradeable contract with a resource account is to use Aptos Move's **init_module** function. The **init_module** function is a unique function that will **only** run the first time a contract is published. It's always a private function with a single argument (the module publisher, in the form of `&signer`) with no return value. @@ -127,10 +127,10 @@ fun init_module(publisher: &signer) { } ``` -We can use this function to retrieve the SignerCapability and store it somewhere for use later: +We can use this function to retrieve the SignerCapability and store it somewhere for use later, but since the resource that stores SignerCapability is not accessible externally, we must write a function call to interface with the SignerCapability. -```rust title="Using init_module to store a publishing resource account's SignerCapability" -module upgradeable_resource_contract::package_manager { +```rust title="Using init_module and publish_package in a contract to function as an interface for the developer" +module upgradeable_resource_account_package::package_manager { use aptos_framework::account::{Self, SignerCapability}; use aptos_framework::resource_account; use aptos_std::code; @@ -155,18 +155,15 @@ module upgradeable_resource_contract::package_manager { }); } - // NOTE: This function is integral to making the contract upgradeable. Without it, the SignerCapability is stored, but - // there is no way to actually use it to upgrade the module. - public entry fun upgrade_module( + // This function is integral to making the contract upgradeable. Without it, the SignerCapability is still stored, but + // there is no way to actually use it to upgrade the module, making the module effectively immutable. + public entry fun publish_package( deployer: &signer, package_metadata: vector, code: vector>, - ) acquires SignerCap { - // NOTE: If we leave this line out, anyone can upgrade the contract and potentially hijack its functionality. + ) acquires PermissionConfig { assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); - let signer_cap = &borrow_global(@upgrade_resource_contract).signer_cap; - let resource_signer = account::create_signer_with_capability(signer_cap); - code::publish_package_txn(&resource_signer, package_metadata, code); + code::publish_package_txn(&get_signer(), package_metadata, code); } } ``` @@ -174,169 +171,10 @@ module upgradeable_resource_contract::package_manager { There are several things going on here to take note of: 1. We retrieve and store the SignerCapability in the `SignerCap` struct. -2. We add an `upgrade_module(...)` function that acts as an interface to the package publishing function. It allows the **deployer** of the module to upgrade it despite not being the direct owner of the module. +2. We add an `publish_package(...)` function that acts as an interface to the package publishing function. It allows the **deployer** of the module to upgrade it despite not being the direct owner of the module. 3. The **deployer** is the developer's account- the one that calls `create_resource_account_and_publish_package(...)` and owns the resource account but not the module itself. It is a named address here, signified with `@deployer`, so we must specify it as a named address upon publication. -4. We gate access to the `upgrade_module` function by asserting that the signer is the original `@deployer`. +4. We gate access to the `publish_package` function by asserting that the signer is the original `@deployer`. Publishing this module from the Aptos CLI would look like this: -```shell -aptos move create-resource-account-and-publish-package \ - --address-name upgrade_resource_contract \ - --named-addresses owner=CONTRACT_DEPLOYER \ - --profile CONTRACT_DEPLOYER -``` - -Where `CONTRACT_DEPLOYER` is the profile. Read more about [Aptos CLI profiles here.](../../tools/aptos-cli/use-cli/use-aptos-cli#creating-other-profiles) - -### Step-by-step guide - -Let's run through an example of how to publish the above upgradeable contract to a resource account and upgrade it. - -1. Publish the module to a resource account -2. Run the `upgradeable_function` view function and see what it returns -3. Upgrade the module using the json output from the `aptos move build-publish-package` command -4. Run the `upgradeable_function` view function again to see the new return value - -First make sure you have a default profile initialized to devnet. - -```shell -aptos init --profile default -``` - -Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `default` in our commands, it will automatically use this profile. - -Navigate to the `move-examples/upgrade_resource_contract` directory. - -### Publish the module - -```shell -aptos move create-resource-account-and-publish-package --address-name upgrade_resource_contract --seed '' --named-addresses owner=default -``` - -The `--address-name` flag denotes that the resource address created from the resource account we make will be supplied as the `upgrade_resource_contract` address in our module. Since we declared it as the module address with `module upgrade_resource_contract::upgrader { ... }` at the very top of our contract, this is where our contract will be deployed. - -When you run this command, it will ask you something like this: - -``` -Do you want to publish this package under the resource account's address be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? [yes/no] > -``` - -Say yes and copy that address to your clipboard. That's our resource account address where the contract is deployed. - -Now you can run the view function! - -### Run the view function - -```shell -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function -``` - -Remember to replace `RESOURCE_ACCOUNT_ADDRESS` with the resource account address you deployed your module to; it is different from the one posted above, so this will specifically only work for *your* contract. - -It should output: -```json -Result: [ - 9000 -] -``` - -### Change the view function - -Now let's change the value returned in the view function from `9000` to `9001` so we can see that we've upgraded the contract: - -```rust -#[view] -public fun upgradeable_function(): u64 { - 9001 -} -``` - -Save that file, and then use the `build-publish-package` command to get the bytecode output in JSON format. - -### Get the bytecode for the module - -```shell -aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgrade_resource_contract=RESOURCE_ACCOUNT_ADDRESS,owner=default -``` - -Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. - -The hex values in this JSON file are arguments that we'd normally use to pass into the `0x1::code::publish_package_txn` function, but since we made our own `upgrade_contract` function that wraps it, we need to change the function call value to something else. - -Your JSON should look something like the below output, just with expanded `value` fields (truncated here for simplicity's sake): - -```json -{ - "function_id": "0x1::code::publish_package_txn", - "type_args": [], - "args": [ - { - "type": "hex", - "value": "0x2155...6200" - }, - { - "type": "hex", - "value": [ - "0xa11c...0000" - ] - } - ] -} -``` - -Change the `function_id` value in the JSON file to match your contract's upgrade function contract, with your resource account address filled in: - -``` -"function_id": "RESOURCE_ACCOUNT_ADDRESS::upgrader::upgrade_contract", -``` - -Save this file so we can use it to run an entry function with JSON parameters. - -### Run the upgrade_contract function - -```shell -aptos move run --json-file upgrade_contract.json -``` - -Confirm yes to publish the upgraded module where the view function will return 9001 instead of 9000. - -### Run the upgraded view function - -```shell -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function -``` - -You should get: - -```json -Result: [ - 9001 -] -``` - -Now you know how to publish an upgradeable module to a resource account! - -## Creating and funding a resource account - -Another common usage is to create and fund a resource account, in case the account needs access to functions that need access to `Coin`: - -```rust -// resource_account.move -public entry fun create_resource_account_and_fund( - origin: &signer, - seed: vector, - optional_auth_key: vector, - fund_amount: u64, -) acquires Container { - let (resource, signer_cap) = account::create_resource_account(origin, seed); - coin::register(&resource); - coin::transfer(origin, signer::address_of(&resource), fund_amount); - rotate_account_authentication_key_and_store_capability( - origin, - resource, - signer_cap, - optional_auth_key, - ); -} -``` +If you're looking for an example of how to do this, see the [publishing an upgradeable module example.](./publishing-an-upgradeable-module.md) diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index 4a99a3a5362b9..dbb2fbe6da124 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -356,7 +356,8 @@ const sidebars = { collapsed: true, items: [ "guides/resource-accounts/utilizing-resource-accounts", - "guides/resource-accounts/understanding-resource-accounts", + "guides/resource-accounts/publishing-an-upgradeable-module", + "guides/resource-accounts/common-questions", ], }, ], From e50b5eeed7d9ac1a67a65ae450c26b96d45ff9f4 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:52:04 -0700 Subject: [PATCH 08/10] Updated commands to reflect new named addresses and fixed some typos. Tutorial should work with copy paste --- .../upgrade_contract.json | 9 ++--- .../publishing-an-upgradeable-module.md | 34 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json b/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json index 2ab16336189c8..8bd00e7dddf1f 100644 --- a/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json +++ b/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json @@ -1,16 +1,17 @@ { - "function_id": "be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9::upgrader::upgrade_contract", + "function_id": "f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e81::package_manager::publish_package", "type_args": [], "args": [ { "type": "hex", - "value": "0x2155706772616465205265736f75726365204163636f756e7420436f6e747261637401000000000000000040423434313945414132343332454643453639443743433842333935364336454333363833344536343044434244314437343732323334363743434635414545329d011f8b08000000000002ff3d8dcd0ac2301084ef798a25775b5fc043117c00c15329b26ed6129a66437e1411dfdd04b1cc5c86f9861903d282334fcae3ca70007d097344c370e6242512c34024c567388acf11296bf5e098acf846efbb2aadca6f740de22cbd5a41b206ccf6e6582b351a0eec0d7bb29c2635842ce914ebe153e252e9373821746dd7757df5fd5ff6d8d8dd96357cbe9710eb1ab3000000010875706772616465729d041f8b08000000000002ff8d534d6fdb300cbdf7577018d0d94090a6c0306c4e5314d86987edb2e330188ccc24421dc9d347d2acc87f9fa428f2479d603ec9241f1ff9f4b49595ad096cb3565851a9484bab18954c0aa39099a2882905af37e03eab09b4a98a42f3b52035ef079131698543bdfe0ce9afd8e092d7dc1c8e83cac41421e38d0651262b9adf849836ca3203df0f4322d8a086673ac479fd97b84e33970c9b0286b849283f9ebaafac002eb829b7419f6cd0a180dbd321efb0a0d6a4ccbb2c961458550ea64bb91ac273582ce0e9a2e61398e5f3d4b62633b6002c6028a117d5284ebb4ecf98f288e1141378927b3f4dcbb5950e6be4dbca1199dbc52f4a3c129c24d431f246c5efeee0873404664351492e85fb4313420cebda7990eb38f414bead604f4e1ddc79904bd45c1048ebe4437190c263c4d9d8a71e495f7a69a4e6621dc2e729216af541a79076adaa1ed63b83f9d1d08b300da33776597306e42a0ec139e79b3d83b2b474983db9a715634b062b3458bac5b96bfd97aa0276c48c540ff6f3635be85f40cab405b122773bfcb1dc8d7ffdc6ae58f5648960d0709cc0fd7f99f176299592fb725dcb25d60f6fe91fb3cb8ecfa7234dafb23ac6e47aa6084d42eeb9d97878a4cd463a77160a7a16e106f5a66c903de3da3d811791f5ec7d3b7c11bdecc8edf50b3c4b1be91bfffdaf1da7fdefae933a1ec2654de5d973595e80fdf4b173915f66b3fbd8ecf80f9d1d74aacb05000000000300000000000000000000000000000000000000000000000000000000000000010e4170746f734672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200" + "value": "0x285570677261646561626c65204d6f64756c652057697468205265736f75726365204163636f756e740100000000000000004045463342424543333046313937354134464542394231314335394242453043314439354231424541373838384139343338314241383033334235433037393633cb011f8b08000000000002ff4d8e410ec2201045f7730ac2deaa0770e1c69d1b13e3a231648451899421d0aa8df1ee42db34061202ccffefd501f5036f74068f0d898d90c7708b68082f8ec49e4d978f936defe24089bba8496cb5e6ceb7129e1493655f42ab2a2f09dd9855819dd57df9d0dc046c6d2e9300351a1329254a67c0d07252d798a92f8e8fa1e4bd96602838ee2996073537161b15270385a3819adca751a87396bc21af6d216c0b61f707f808c71a5d19afaa65de337d39d82ce6bb145ff801354bc5571a010000020f7061636b6167655f6d616e61676572ac081f8b08000000000002ffad56db8edb36107dcf574c50c0900a6337058aa2d05eb0c53640f3d21459f7a5454150d2c8662d890a49add709fcef1992bacb4e9c4dfc625b9adb3967389c42a6758e50576bc553e4718e4ca196b54a90f12491756958c5932d5f6314353f58c14bfa52f0f105d0a7d608bc3252b34cf10277526da3a8f18da28f0f98674b7810eb12d53daf782c7261f687abd3bed302a6a6daa451a40bae0c33b6e23e897db6b28f06e1bdb551a25c47d183fb3e1a3091294ebc5029a9a6911c10ff305302cbf44cf262ae45c212591ac513c2e4225c5e5ec26a43a15d54101a4a6980d7662395f8802918092956b9dc83d9d0dbc2c975e17c299436f0facfb72bf6dbdfab3fdebe7bf3cfebdf23a87ff9196ee0d520c18391541654a80aa1b590a575cdc41a749d6c80eb9938904905ae5299e7c417e54668914183cc1741c4d68981bfbad8f73ef486c26e71dfb4485bc93b7c5f0be561c9d870511e0dddd071d1f9faff2ce155342b7639cab02272b72e284f538aab0978a2901b4a1aefdd0bcfa106979c386d14eab3759ed1a0a3ae7deb2cdbb7b73eefa1a7f94d298ce039c936a783f0a2b6cd2af4a66516e423497e9a5adf1b5460569789b1b2095bf4a3dc121659e67bd86d708cc15af876b11c53e44c28ea11238a061e85a210c2304f42d0b5ab67388285ff110e84cbd10c04a0de9af6b83db1440e3e1e697ff2982659c25d53a30aafba2c05b1c18c9c1bcfb8ec2b9b88351a0925ee669205e172e4dba3ea9f1f9a9a1a61ab9a344b0049b1bd63cf3dd09bf65c079d638ba9e3b08fd9cd4d343ce58647f088091dcaebfad7dbdeca0ea0ee4d6fd05884d41ceef0e8cf31c28909655e068d9c51839cc92ce838879b9b5e8125b82147b3bd0bca522c05a6c164b28403b15ca9d1840a669eca60b146d348476ccf902f9deb9862dbe7f7bc841821e179ee0faa1fadf4bb3dad5f3131bc66810f113ad5865545edb43d87d059f32f624984edd83a9731cfafa7aeb7c1dd39d74178d107ed69edce939f584dc56c27ccc61a36032fe85dc367f1b845acc0b8412933e0a0f7da60d19e92931cd27b56d2259db2c632b0ff684636a74cc6ff53d3466d9cf06c7efb494df416b58111c58c9e3c9fe62e76cff2684cd0fba0b3598245d422393a082c112312183e096dc65c508bc59206fc39048caab13703b5b80e169a67c80667d2dfd8c100902f363c51a36df8d362859d4acf13e93b9d8123e2fc38e2c3e7093e875994b4a1a0c37c8a339ac83370e7c0fe3e2887c5fef0afa13580d9cbfbbfa962a25b1f18ad5fcc1a06b33b657a2fb7064e655266b0a21e1ffe3dd32283e0a56fde23d84681c3707ae94e5776bb61d07e5354949d3acffe65b402d2664d6a5213b688a8f782c58931d7a1be7bf5f493bd6dc629274e2dcd33ae7cc55723e776b9e86fbd2f6c15dfba594c97d669f5ae0d7a8349f1e358870198433b930e2f3e01d480aa05ba0d000000000e62617369635f636f6e7472616374e0031f8b08000000000002ff8d52df6bdb400c7ecf5fa131e86c084d36ca602e8115dab1bd2cd0750fdb18d78b4f718eda27ef7ea4cb4afef729e7b317672f3d30be93f4499f3ea921156a84d056562a94ab1a854547c19628645952305eb4b27c901516c54a3a5d8a928cb7b2f4f034013ec13d179e2ea291867ff672803baf8a02ada5539bd395390446abf33670d52fd4e06daa011be9e0017789cae16c651db080f0f6621a6dfb0e3c9bcde01b059016c1900719fc86acfe830a3c418b764db601bfd10eb8354de63cc2b857e7e1e6f3f24e5c7dbdfbb8bcfdf4fde63a668705cc13b1973fb61a1f7fc67b1b56b52e611dcc48147ec7ac59de81fff17d379fbf1e13852b03f85b362dcf85d6a0489b0a1c77cdecf8f6a8fd868922f44243129a8dd2b3f557d0ec02ed1d74fa1d13439edd2ed26b688bc2d37ff3ca14b635edd01670d6e1f323ba4c6f69ea5d24d00742290d7f75dde9d7f77a3e80a47368fd8bac4b57145229aeea04ad8762392c16f0be7f4d216e03af0cda463bc7d98442a3516527b3c8f3348444ee9a9e23d5ab913409fa816caffb142407c409dc279dee6387437c8d7ec82aba5cbc11271b5e1415fae4cdf2cb019c526683e170ce4ed24d47ded1d63f8d5c475b7ff1668cda0faf547d3fd94ffe02f152d31bf303000000000300000000000000000000000000000000000000000000000000000000000000010e4170746f734672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200" }, { "type": "hex", "value": [ - "0xa11ceb0b060000000b01000a020a0803122305351d0752f30108c502400685034410c9032f0af803060cfe036e0dec04020000010101020103010400050800010a0600000600010000070201000008010300040b000500030c060400010d080700020e02010001060c0003060c0a020a0a020103010801010502060c05010c01060801087570677261646572076163636f756e7404636f6465107265736f757263655f6163636f756e74067369676e6572124d795369676e65724361706162696c6974790b696e69745f6d6f64756c6510757067726164655f636f6e7472616374147570677261646561626c655f66756e6374696f6e137265736f757263655f7369676e65725f636170105369676e65724361706162696c6974790a616464726573735f6f661d72657472696576655f7265736f757263655f6163636f756e745f6361701d6372656174655f7369676e65725f776974685f6361706162696c697479137075626c6973685f7061636b6167655f74786e5189d8b84793e93065f28893ab3e01cf5883e5daf93404d567a79be29958e423000000000000000000000000000000000000000000000000000000000000000105205189d8b84793e93065f28893ab3e01cf5883e5daf93404d567a79be29958e4230520f43c3dbad12a67d2c242a3e3bbbc8718cde26b36f75c075e96bf55dda9b0af76126170746f733a3a6d657461646174615f76311b000001147570677261646561626c655f66756e6374696f6e0101000002010908010000000004130a0011030700210406050a0b0001060000000000000000270a00070111040c010b000b0112002d0002010104010007120b001103070121040605080601000000000000002707002b00100011050c030e030b010b0211060202010000010206292300000000000002000000" + "0xa11ceb0b060000000d010010021014032453047708057f5707d6018b0308e1044006a1054e10ef05630ad2060d0cdf0692010df107040ff507020001010201030104010501060107010800090800070a0700011206000614040200000000000b000100000c020300000d010400000e050100000f0206000010070100061508010200000616090a02020001170b040004180d0e000619010f020704061a0906020200051b050300031c101000021d070100060007000a000b0002080105000108010105010c01060c010103060c0a020a0a0203070b0302090009010900090102060b03020900090109000106090101060802040b03020801050802060c080202060c05010802010b03020900090101030e62617369635f636f6e74726163740f7061636b6167655f6d616e61676572076163636f756e7404636f6465056572726f72107265736f757263655f6163636f756e74067369676e65720b736d6172745f7461626c6506737472696e67105065726d697373696f6e436f6e66696706537472696e67116164645f6e616d65645f61646472657373116765745f6e616d65645f616464726573730a6765745f7369676e65720b696e69745f6d6f64756c65146e616d65645f616464726573735f6578697374730f7075626c6973685f7061636b6167650a7369676e65725f636170105369676e65724361706162696c697479096164647265737365730a536d6172745461626c650361646406626f72726f771d6372656174655f7369676e65725f776974685f6361706162696c6974791d72657472696576655f7265736f757263655f6163636f756e745f636170036e657708636f6e7461696e730a616464726573735f6f66117065726d697373696f6e5f64656e696564137075626c6973685f7061636b6167655f74786ef409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e810000000000000000000000000000000000000000000000000000000000000001030800000000000000000520f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e8105206fc8d6b07117b68c1cca1f276d25f5287371d012c3018f248d86b950a51da25e126170746f733a3a6d657461646174615f76314f0100000000000000000f454e4f545f415554484f52495a454433546865207369676e6572206973206e6f7420617574686f72697a656420746f206465706c6f792074686973206d6f64756c652e0000000202110802130b03020801050003000100010707012a000f000b000b013800020101000100010707012b0010000b00380114020203000100010507012b001001110802030000000c100a00070211090c040b000c0338020c010b040c020b030b020b0112002d00020401000100010607012b0010000b00380302050104010004100b00110c070221040605090700110d2711020c030e030b010b02110e0200010000000000", + "0xa11ceb0b060000000a010008020804030c1905250a072f950108c401400684022c10b002760aa603050cab033a0000010101020003000408000005000100000601020002080004000109020200030a01030001060c000103010c01050e62617369635f636f6e7472616374056572726f72067369676e65720f7061636b6167655f6d616e616765720c536f6d655265736f75726365186d6f76655f746f5f7265736f757263655f6163636f756e74147570677261646561626c655f66756e6374696f6e0576616c75650a616464726573735f6f66117065726d697373696f6e5f64656e6965640a6765745f7369676e6572f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e8100000000000000000000000000000000000000000000000000000000000000010308000000000000000005206fc8d6b07117b68c1cca1f276d25f5287371d012c3018f248d86b950a51da25e126170746f733a3a6d657461646174615f7631620100000000000000000f454e4f545f415554484f52495a45442e596f7520617265206e6f7420617574686f72697a656420746f20706572666f726d207468697320616374696f6e2e0001147570677261646561626c655f66756e6374696f6e01010000020107030001040003100b00110207012104060509070011032711040c010e01062a0000000000000012002d00020101000001020629230000000000000200" ] } ] -} \ No newline at end of file +} diff --git a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md index d97a9a4b1b878..682acf4838dfa 100644 --- a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md +++ b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md @@ -26,24 +26,25 @@ Each of the following steps will be explained in detail in their corresponding s 5. Run the `publish_package` function to upgrade the module 6. Run the view function again to observe the new return value -First make sure you have a `deployer` profile initialized to devnet. +First navigate to the correct directory: +```shell title="Navigate to your local directory" +cd ~/aptos-core/aptos-move/move-examples/upgradeable_resource_account_package +``` +Then create a `deployer` profile initialized to devnet: ```shell -aptos init --profile `deployer` +aptos init --profile deployer ``` -Choose `devnet` and leave the private key part empty so it will generate an account for you. When we write `deployer` in our commands, it will automatically use this profile. - -```shell title="Navigate to your local directory" -cd ~/aptos-core/aptos-move/move-examples/upgradeable_resource_account_package -``` +Enter devnet when prompted and leave the private key empty so it will generate an account for you. When we write `deployer` in our commands, it will automatically use this profile. ### 1. Publish the module ```shell -aptos move create-resource-account-and-publish-package \\ - --address-name upgradeable_resource_account_package --seed '' \\ - --named-addresses owner=deployer +aptos move create-resource-account-and-publish-package \ + --address-name upgradeable_resource_account_package --seed '' \ + --named-addresses deployer=deployer \ + --profile deployer ``` The `--address-name` flag marks the following string as named address the resource account's address will appear as in the contract. @@ -65,7 +66,7 @@ Enter yes and copy that address down somewhere. That's our resource account addr Replace `RESOURCE_ACCOUNT_ADDRESS`` with the address you got from step #1 and run the following command: ```shell title="View the value returned from upgradeable_function()" -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::upgrader::upgradeable_function +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function --profile deployer ``` It should output: @@ -91,17 +92,16 @@ Save that file, and then follow step #4 to get the package metadata and bytecode ### 4. Get the new bytecode for the module ```shell -aptos move build-publish-payload --json-output-file upgrade_contract.json \\ - --named-addresses upgradeable_resource_account_package=RESOURCE_ACCOUNT_ADDRESS,owner=deployer +aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgradeable_resource_account_package=RESOURCE_ACCOUNT_ADDRESS,deployer=deployer ``` Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. Since we made our own `upgrade_contract` function that wraps the `0x1::code::publish_package_txn`, we need to change the function call value to our publish package function. -After editing the JSON output, your file should look something like below, except you'll have much longer expanded `value` fields (truncated here for simplicity's sake): +After editing `upgrade_contract.json`, your `function_id` value should look something like below: -```json +```json title="Change the function_id value in upgrade_contract.json to call your resource account's publish package function" { "function_id": "RESOURCE_ACCOUNT_ADDRESS::package_manager::publish_package", "type_args": [], @@ -125,7 +125,7 @@ Make sure to change the `RESOURCE_ACCOUNT_ADDRESS` to your specific resource acc ### 5. Run the upgrade_contract function ```shell -aptos move run --json-file upgrade_contract.json +aptos move run --json-file upgrade_contract.json --profile deployer ``` Confirm yes to publish the upgraded module. @@ -133,7 +133,7 @@ Confirm yes to publish the upgraded module. ### 6. Run the upgraded view function ```shell -aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function --profile deployer ``` You should get: From c1111245727eb7cf0ec15b8c410b08f7e39db3c5 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:45:52 -0700 Subject: [PATCH 09/10] Updating Examples => Guides, adding several examples and explanation of analogous real world use case, and explanation of seed --- .../resource-accounts/common-questions.md | 29 ++++++--- .../docs/guides/resource-accounts/index.md | 3 +- ...ounts.md => managing-resource-accounts.md} | 12 ++-- .../docs/guides/resource-accounts/overview.md | 65 +++++++++++++++++++ .../publishing-an-upgradeable-module.md | 4 ++ developer-docs-site/docusaurus.config.js | 4 +- developer-docs-site/sidebars.js | 13 ++-- 7 files changed, 109 insertions(+), 21 deletions(-) rename developer-docs-site/docs/guides/resource-accounts/{utilizing-resource-accounts.md => managing-resource-accounts.md} (93%) create mode 100644 developer-docs-site/docs/guides/resource-accounts/overview.md diff --git a/developer-docs-site/docs/guides/resource-accounts/common-questions.md b/developer-docs-site/docs/guides/resource-accounts/common-questions.md index 5c2e7ee2c79d0..49765d193fb55 100644 --- a/developer-docs-site/docs/guides/resource-accounts/common-questions.md +++ b/developer-docs-site/docs/guides/resource-accounts/common-questions.md @@ -7,16 +7,11 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Common questions - -## What is a resource account? - -A [resource account](../../move/move-on-aptos/resource-accounts.md) is an [Account](../../concepts/accounts/) that's used to store and manage resources independent of a user. It can be used as a simple storage account or it can be utilized to programmatically manage resources from within a smart contract. - ## How are resource accounts created? Let's review the two functions used to create resource accounts and what they return. -First off, note that both creation functions allow for the input of a `seed` byte vector for the [ensuing hash used to compute the resource address](#how-is-the-address-for-a-resource-account-derived). +First off, note that both creation functions allow for the input of a [**seed**](./common-questions.md#whats-a-seed) byte vector for the [ensuing hash used to compute the resource address](#how-is-the-address-for-a-resource-account-derived). @@ -65,12 +60,22 @@ public entry fun create_resource_account( ``` The resource account created from this is functionally very similar to a user account that has had its authentication key rotated (see: [Rotating an authentication key](../account-management/key-rotation.md)), because it cannot yet be controlled programmatically and can still be controlled by a private key. -However, there does exist a SignerCapability for the resource account, it just isn't being used yet. To enable programmatic control, you would need to [retrieve the SignerCapability.](./utilizing-resource-accounts#retrieving-a-signercapability) +However, there does exist a SignerCapability for the resource account, it just isn't being used yet. To enable programmatic control, you would need to [retrieve the SignerCapability.](./managing-resource-accounts#retrieving-a-signercapability) The end result of a creating a resource account with `create_resource_account(...)` in `resource_account.move` and then retrieving the SignerCapability is the same as creating the resource account with `create_resource_account(...)` in `account.move`. +## What's a seed? + +A seed is an optional user-specified byte vector that is input during the creation of a resource account. The seed is used to ensure that the resulting resource account's address is unique. + +Since the hash function used to derive the resource account's address is deterministic, providing the same seed and source address will always result in the same resource account address. + +This also means that without a seed, if you were to try to generate multiple resource accounts from a single source account, you would end up with the same address each time due to a collision in the hashing computation. + +Thus, the `seed: vector` argument facilitates the creation of multiple resource accounts from a single source account and also allows for deterministically deriving a resource account address given an address and a seed byte vector. + ## What's a SignerCapability? A SignerCapability is a simple but powerful resource that allows a developer to programmatically manage a resource account. This is achieved with the `create_signer_with_capability` function: @@ -124,7 +129,7 @@ fun internal_get_signer(): signer { ## How is the address for a resource account derived? -When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address, a byte scheme to identify it as a resource account, and an optional user-specified byte vector `seed`. Here is the implementation of `create_resource_address` function in `account.move`: +When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address, a byte scheme to identify it as a resource account, and an optional user-specified byte vector [**seed**](./common-questions.md#whats-a-seed). Here is the implementation of `create_resource_address` function in `account.move`: ```rust /// This is a helper function to compute resource addresses. Computation of the address /// involves the use of a cryptographic hash operation and should be use thoughtfully. @@ -135,3 +140,11 @@ public fun create_resource_address(source: &address, seed: vector): address from_bcs::to_address(hash::sha3_256(bytes)) } ``` + +## Why am I getting the `EACCOUNT_ALREADY_EXISTS` error? + +If you're getting this error while trying to create a resource account, it's because you have already created a resource account with that specific [**seed**](./common-questions.md#whats-a-seed). + +This error occurs because there's a collision in the output of the hashing function used to derive a resource account's address. + +To fix it, you need to change the seed or the function you're calling will continue to unsuccessfully attempt to create an account at an address that already exists. diff --git a/developer-docs-site/docs/guides/resource-accounts/index.md b/developer-docs-site/docs/guides/resource-accounts/index.md index a90d3cfe1e7b1..4d1a37bd5d56d 100644 --- a/developer-docs-site/docs/guides/resource-accounts/index.md +++ b/developer-docs-site/docs/guides/resource-accounts/index.md @@ -4,6 +4,7 @@ title: "Resource Accounts" # Resource Accounts -- ### [Utilizing resource accounts](./utilizing-resource-accounts) +- ### [Overview](./overview) +- ### [Managing resource accounts](./managing-resource-accounts) - ### [Publishing an upgradeable module](./publishing-an-upgradeable-module) - ### [Common questions](./common-questions) diff --git a/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md similarity index 93% rename from developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md rename to developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md index 3f928e1d06099..8f720c913d006 100644 --- a/developer-docs-site/docs/guides/resource-accounts/utilizing-resource-accounts.md +++ b/developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md @@ -1,14 +1,14 @@ --- -title: "Utilizing resource accounts" -id: "utilizing-resource-accounts" +title: "Managing resource accounts" +id: "managing-resource-accounts" --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# General usage +# Managing resource accounts -In this section we're going to explain the mechanisms behind general usage of resource accounts and how we can use them to automate smart contracts and manage resources programmatically. +In this section we're going to explore the various mechanisms underlying resource accounts and how to utilize them to manage resources programmatically. There are two distinct ways to manage a resource account: @@ -85,6 +85,10 @@ Without access to a `SignerCapability`, there is no way to generate a signature ## Publishing modules with resource accounts +Publishing modules with resource account gives developers the ability to separate the logic and resources of their smart contracts from their normal user accounts. It also offers them the ability to publish immutable, open source contracts that other developers can use without fear of the contract being altered. + +Below we detail the various ways to publish a module to a resource account. + ### Publishing an immutable module with a resource account One of the most common usages of resource accounts is publishing a module with them. This function in `resource_account.move` is called by a user account to create a resource account and publish a module with it. diff --git a/developer-docs-site/docs/guides/resource-accounts/overview.md b/developer-docs-site/docs/guides/resource-accounts/overview.md new file mode 100644 index 0000000000000..1b6d7b47f502a --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/overview.md @@ -0,0 +1,65 @@ +--- +title: "Overview" +id: "overview" +--- + +# Overview + +## What is a resource account? + +A [resource account](../../move/move-on-aptos/resource-accounts.md) is an [account](../../concepts/accounts/) that's used to store and manage [resources](../../concepts/resources/) independent of a user. It can be used as a simple storage account or it can be utilized to programmatically manage resources from within a smart contract. + +In a more general sense, the programmable management of resource accounts facilitates a trustless exchange model between two parties. Resource accounts act as trustless, programmable third-party escrows, which are a fundamental building block in the creation of smart contracts. + +## How are resource accounts used? + +Resource accounts are used in two ways: + +1. General resource management, like using them to [publish smart contracts to a non-user account](./managing-resource-accounts#publishing-modules-with-resource-accounts), and +2. Automating resource management in a smart contract by generating the [signer](../../move/book/signer.md) for an account without a private key + +Sometimes when writing smart contracts, developers need to programmatically automate [the approval of an account](../../concepts/accounts.md#access-control-with-signers) delegated to resource management in order to create seamless smart contracts that only require one call. + +Resource accounts can relinquish their ability to generate a signature with a [private key](https://en.wikipedia.org/wiki/Public-key_cryptography) by delegating it to a [**SignerCapability**](./managing-resource-accounts#using-a-signercapability) resource, which is used to generate an on-chain `signer` primitive for the account. + +:::tip +As we may seem to refer to them interchangeably, it's important to note that an account's signature for a transaction is not exactly the same thing as a `signer`. In the context of a Move module (aka a smart contract), a `signer` is the on-chain, primitive data type used to represent an account's approval to process a given transaction, whereas a signature is the off-chain representation of this approval. +::: + +Think of it this way: the ***signature*** for a transaction function `public entry fun foo(sender: &signer)` is converted to a ***signer*** and represented as the input argument `sender` in an [entry function.](../../move/book/functions#entry-modifier) + +## A real-world use case: a vesting contract + +A [vesting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/framework/aptos-framework/sources/vesting.move#L929) is a smart contract that automates the timed disbursement of allotted funds to a designated receiver. + +A real world analogous example of this is a trust fund where a child receives money over time from their grandparents, or a compensation package offered by a company that rewards an employee with vested stock options over time. + +To explore conceptually how we'd create something like this with a smart contract, let's evaluate the analogous equivalents in terms of who is sending and receiving funds. + +In a trust fund, there is a sending party (the grantor), the receiving party (the beneficiary), and the third-party (the trustee) that handles the disbursement of funds. Smart contracts can be employed to programmatically manage funds, effectively acting as the third-party trustee in this example. + +The general process for this is: + +1. The grantor locks up the initial funds and sets rules on how much the beneficiary can receive over time +2. As time passes, the beneficiary requests funds +3. If there are funds available based on the amount of time that has passed, the funds are transferred to the beneficiary +4. The beneficiary repeats step 3 periodically until the funds are depleted + +The two active parties here are the trustee and the beneficiary. Conceptually, we can think of these two parties as two separate accounts on-chain. Managing an account's resources requires the signed approval of the account, meaning a smart contract that facilitates periodically releasing vested funds would require the approval of both the sender and the receiver accounts whenever funds are to be dispersed. + +Requiring multiple signers to call an [entry function](../../move/book/functions/#entry-modifier) is logistically complex not only for a developer but for the two users as well. Both of the users would need to sign off on the transfer transaction every vesting period. Since these would be asynchronous, manual approvals, one user calling the function will always be waiting on the other. + +However, with resource accounts, we can integrate programmatic control of these funds, gating access to the funds with the smart contract's internal logic. Whenever the receiver requests to receive funds, the smart contract evaluates if there are any funds to disperse based on time and the amount of funds left. If there are funds ready to be dispersed, the contract internally generates a signer for the resource account holding the funds in order to withdraw them from it. + +This internally generated `signer` primitive is created with the [**SignerCapability**](./managing-resource-accounts#using-a-signercapability) resource, thus fully automating the process for the receiver- completing the exchange of funds with only one account's signature! + +## More examples of how to use resource accounts +- [Defi swap contract](https://github.com/aptos-labs/aptos-core/blob/c1d87b8a3f17059311d4bdf83b953d12c61c14c0/aptos-move/move-examples/resource_account/sources/simple_defi.move#L39): an automated escrow contract where the resource account acts as an automated and trustless 3rd party escrow. + +- [Automated token minting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/move-examples/post_mint_reveal_nft/sources/minting.move#L181C49-L181C65): a minting contract where the resource account is the collection creator and mints/sends out tokens on request. + +- [Multi-signature accounts](https://github.com/aptos-labs/aptos-core/blob/4f9b69b6592f58e57691944b888461c2a93ffe7a/aptos-move/framework/aptos-framework/sources/multisig_account.move#L993): using a resource account as a shared account for multiple users, controlled through decentralized voting mechanisms. + +- [Coin disbursement through a shared account](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/move-examples/shared_account/sources/shared_account.move#L48): a coin disbursement contract where coins sent to a resource account are distributed to multiple accounts according to a fixed percentage. + +- [Vesting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/framework/aptos-framework/sources/vesting.move#L929): a vesting contract that disburses a fixed % of coins over a set amount of time. diff --git a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md index 682acf4838dfa..8e2e909944fea 100644 --- a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md +++ b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md @@ -47,6 +47,10 @@ aptos move create-resource-account-and-publish-package \ --profile deployer ``` +:::info +Read more about the [**seed**](./common-questions.md#whats-a-seed) argument here. +::: + The `--address-name` flag marks the following string as named address the resource account's address will appear as in the contract. That is, the resource account created from this command will correspond to the `@upgradeable_resource_account_package` address in our module. diff --git a/developer-docs-site/docusaurus.config.js b/developer-docs-site/docusaurus.config.js index 5ca15cd3c37c2..25dba04088850 100644 --- a/developer-docs-site/docusaurus.config.js +++ b/developer-docs-site/docusaurus.config.js @@ -159,8 +159,8 @@ const config = { to: "/category/nft", }, { - label: "Examples", - to: "/category/examples", + label: "Guides", + to: "/category/guides", }, { label: "Build E2E Dapp on Aptos", diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index dbb2fbe6da124..d1a239dee22ba 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -336,15 +336,15 @@ const sidebars = { }, { type: "category", - label: "Examples", + label: "Guides", collapsible: true, collapsed: true, link: { type: "generated-index", - title: "Examples", - description: "Examples for all the various concepts and tooling used to build on Aptos.", - slug: "/category/examples", - keywords: ["examples"], + title: "Guides", + description: "Guides for all the various concepts and tooling used to build on Aptos.", + slug: "/category/guides", + keywords: ["guides"], }, items: [ "guides/account-management/key-rotation", @@ -355,7 +355,8 @@ const sidebars = { collapsible: true, collapsed: true, items: [ - "guides/resource-accounts/utilizing-resource-accounts", + "guides/resource-accounts/overview", + "guides/resource-accounts/managing-resource-accounts", "guides/resource-accounts/publishing-an-upgradeable-module", "guides/resource-accounts/common-questions", ], From fa0c6175574a92153a4182c85117146d94058c52 Mon Sep 17 00:00:00 2001 From: xbtmatt <90358481+xbtmatt@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:50:44 -0700 Subject: [PATCH 10/10] Fixing poorly worded sentence. --- developer-docs-site/docs/guides/resource-accounts/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer-docs-site/docs/guides/resource-accounts/overview.md b/developer-docs-site/docs/guides/resource-accounts/overview.md index 1b6d7b47f502a..9ed7be750a420 100644 --- a/developer-docs-site/docs/guides/resource-accounts/overview.md +++ b/developer-docs-site/docs/guides/resource-accounts/overview.md @@ -47,7 +47,7 @@ The general process for this is: The two active parties here are the trustee and the beneficiary. Conceptually, we can think of these two parties as two separate accounts on-chain. Managing an account's resources requires the signed approval of the account, meaning a smart contract that facilitates periodically releasing vested funds would require the approval of both the sender and the receiver accounts whenever funds are to be dispersed. -Requiring multiple signers to call an [entry function](../../move/book/functions/#entry-modifier) is logistically complex not only for a developer but for the two users as well. Both of the users would need to sign off on the transfer transaction every vesting period. Since these would be asynchronous, manual approvals, one user calling the function will always be waiting on the other. +Requiring multiple signers to call an [entry function](../../move/book/functions/#entry-modifier) is logistically complex not only for the developer but for the end users as well. Both of the users would need to sign off on the transfer transaction every vesting period. Since these would be asynchronous, manual approvals, one user calling the function will always be waiting on the other. However, with resource accounts, we can integrate programmatic control of these funds, gating access to the funds with the smart contract's internal logic. Whenever the receiver requests to receive funds, the smart contract evaluates if there are any funds to disperse based on time and the amount of funds left. If there are funds ready to be dispersed, the contract internally generates a signer for the resource account holding the funds in order to withdraw them from it.