Local Storage & Global Storage Operations
Summary
- Move is a Resource-Oriented Programming language focusing on resources rather than accounts
- Local storage uses the
let
keyword for variables with limited scope and lifespan - Global storage provides persistent data storage on the blockchain using a tree-like structure
- Global storage operations include
move_to
,move_from
,borrow_global
,borrow_global_mut
, andexists
- Resources in global storage require the
key
ability and must be owned by an address - The
drop
ability allows resources to be removed from global storage - Move programs can read from and write to global storage, but cannot access external resources
Overview
Move is a Resource-Oriented Programming (ROP
) language, where the entire system centers around resources instead of accounts as in many traditional blockchains, also known as Resource-Centric
.
In the topic below, we will explore how Global Storage and Local Storage work in Move, and how you can create data stored in these two storage types.
Local Storage ( Local Variable )
In Move, local variables operate within a specific scope and have a limited lifespan. They are declared using the let
keyword and possess unique characteristics:
- Scope: Variables are confined to the block where they are defined
- Shadowing: New declarations can overshadow existing variables with the same name
- Mutability: Values can be modified directly or through mutable references
- Flexibility: They can hold various data types, from simple integers to complex structures
module movement::local_storage {
fun local_variables(){
let b: u8;
let c = false;
let d = b"hello world";
let e: u64 = 10_000;
}
}
All the variables mentioned above are created within the local_variables
function. As a result, they only exist within the function's scope. When this function ends, all variables within it will be dropped
.
Additionally, we can create resources and structs as local storage through a struct ability in move
called "drop". This ability allows the struct or resource to be dropped after the function ends, aligning with the characteristics of local variables you've read about above.
For example:
module movement::local_global_storage {
use std::debug::print;
use std::signer;
struct LocalData has drop {
value: u64
}
public entry fun new_local(value: u64) {
let data = LocalData {
value: value
};
let local_var = b"Local Storage Data";
print(&data);
print(&local_var);
}
#[test]
fun test_new_local() {
new_local(10);
}
}
In the new_local
function, you can see that after the LocalData
resource is created, it's not owned by anyone and isn't transferred anywhere. This means that when the new_local function ends, LocalData
will be dropped. For the Move compiler to allow this, LocalData
must have the drop
ability and must not have the key
ability.
Global Storage
Global storage in Move:
- Enables persistent data storage on the blockchain
- Maintains long-term data accessibility across transactions and contracts
- Uses a tree-like structure for efficient organization and retrieval
- Key-value pairing system for precise data management
Move programs interact with global storage by:
- Reading existing data
- Writing new or updated information
Limitations:
- Cannot access external resources (e.g., filesystems, networks)
- Ensures data manipulations occur within the blockchain's controlled environment
- Maintains security and consistency across the system
struct GlobalStorage {
resources: Map<(address, ResourceType), ResourceValue>
modules: Map<(address, ModuleName), ModuleBytecode>
}
Let's examine the example below for a clearer understanding:
module movement::local_global_storage {
use std::debug::print;
struct GlobalData has key {
value: u64
}
public entry fun new_global(signer: &signer, value: u64) {
let data = GlobalData {
value: value
} ;
move_to(signer, data);
}
#[test(account = @0x1)]
fun test_new_global(account: &signer) {
new_global(account, 10);
}
}
In the code above, after initializing GlobalData
and storing it in a variable called data, if you stop here, the compiler will throw an error when you build. This is because GlobalData
contains the key
ability. Consequently, this data needs to be stored in global storage. However, to store it in global storage, you must assign this Resource an owner in the form of a Map. In this case, we'll store it under the signer who called this function. The result of the function will create a resource and transfer it to the address of the caller.
Here's the GlobalStorage
data after you initialize it using the new_global
function:
{
"0x40264b8d01986e70c79999a189e4c4043aad3ec970d00a095cf29b2916eda04d::local_global_storage::GlobalData": {
"value": "10"
}
}
For global data, you can only access it through these native functions provided by Move:
| Operation | Description | Aborts? |
| --- | --- | --- |
| move_to<T>(&signer,T)
| Publish T
under signer.address
| If signer.address
already holds a T
|
| move_from<T>(address): T
| Remove T
from address
and return it | If address
does not hold a T
|
| borrow_global_mut<T>(address): &mut T
| Return a mutable reference to the T
stored under address
| If address
does not hold a T
|
| borrow_global<T>(address): &T
| Return an immutable reference to the T
stored under address
| If address
does not hold a T
|
| exists<T>(address): bool
| Return true
if a T
is stored under address
| Never |
Example Code:
module movement::local_global_storage {
use std::debug::print;
use std::signer::address_of;
struct GlobalData has key {
value: u64
}
const EResourceNotExist: u64 = 33;
public entry fun new_global(signer: &signer, value: u64) {
let data = GlobalData {
value: value
} ;
move_to(signer, data);
}
public entry fun change_value_from_global_storage(signer: &signer, value: u64) acquires GlobalData {
let addr = address_of(signer);
if (!check_global_storage_exists(addr)) {
abort EResourceNotExist
};
let value_reference = &mut borrow_global_mut<GlobalData>(addr).value;
*value_reference = *value_reference + value;
}
public fun check_global_storage_exists(addr: address): bool {
exists<GlobalData>(addr)
}
#[view]
public fun get_value_from_global_storage(addr: address): u64 acquires GlobalData {
if (!check_global_storage_exists(addr)) {
abort EResourceNotExist
};
let value_reference = borrow_global<GlobalData>(addr);
value_reference.value
}
#[test(account = @0x1)]
fun test_new_global(account: &signer) {
new_global(account, 10);
}
}
Delete Resource
The move_from
function is a crucial part of resource management in Move. It allows for the removal of a resource from an account or address. However, there's an important caveat: the resource must have the "drop" ability to be used with move_from
. This requirement serves as a safety mechanism, preventing accidental or unauthorized deletion of resources.
Key points to understand:
- Resources without the "drop" ability cannot be removed, ensuring their permanence.
- This feature gives developers fine-grained control over resource lifecycle management.
- It's particularly useful for creating persistent resources that should remain intact throughout a contract's lifetime.
By implementing this safeguard, Move enhances the security and predictability of smart contracts, allowing developers to design more robust and controlled resource management systems.
- Drop Ability
struct GlobalData has key, drop {
value: u64
}
- move_from
public entry fun remove_resource_from_global_storage(account: &signer) acquires GlobalData {
let rev = move_from<GlobalData>(address_of(account));
}
Full Code
module movement::local_global_storage {
use std::debug::print;
use std::signer::address_of;
struct GlobalData has key, drop {
value: u64
}
const EResourceNotExist: u64 = 33;
const ENotEqual: u64 = 10;
public entry fun new_global(signer: &signer, value: u64) {
let data = GlobalData {
value: value
} ;
move_to(signer, data);
}
public entry fun change_value_from_global_storage(signer: &signer, value: u64) acquires GlobalData {
let addr = address_of(signer);
if (!check_global_storage_exists(addr)) {
abort EResourceNotExist
};
let value_reference = &mut borrow_global_mut<GlobalData>(addr).value;
*value_reference = *value_reference + value;
}
public fun check_global_storage_exists(addr: address): bool {
exists<GlobalData>(addr)
}
#[view]
public fun get_value_from_global_storage(addr: address): u64 acquires GlobalData {
if (!check_global_storage_exists(addr)) {
abort EResourceNotExist
};
let value_reference = borrow_global<GlobalData>(addr);
print(&value_reference.value);
value_reference.value
}
public entry fun remove_resource_from_global_storage(account: &signer) acquires GlobalData {
let rev = move_from<GlobalData>(address_of(account));
}
#[test(account = @0x1)]
fun test_new_global(account: &signer) {
new_global(account, 10);
}
#[test(account = @0x1)]
fun test_change_value_global(account: &signer) acquires GlobalData {
new_global(account, 10);
change_value_from_global_storage(account, 10); // value should be equal 20
let value = get_value_from_global_storage(address_of(account));
assert!(value == 20, ENotEqual);
// remove resource
remove_resource_from_global_storage(account);
assert!(!check_global_storage_exists(address_of(account)), EResourceNotExist);
}
}
Running test:
movement move test -f local_global_storage
Result:
Running Move unit tests
[debug] 20
[ PASS ] 0x5fdf6936671d4e4a89b686aff0b5a4dfe083babbaaa6e78f5daa8801f94938a6::local_global_storage::test_change_value_global
[ PASS ] 0x5fdf6936671d4e4a89b686aff0b5a4dfe083babbaaa6e78f5daa8801f94938a6::local_global_storage::test_new_globalTest result: OK. Total tests: 2; passed: 2; failed: 0
{
"Result": "Success"
}
Understanding the 'acquires' Keyword in Move
The 'acquires' keyword in Move is an important concept related to global storage operations. Here's what you need to know about it:
Purpose of 'acquires'
The 'acquires' keyword is used to declare that a function may access (or "acquire") a specific resource from global storage. It's a way of explicitly stating which global resources a function intends to use.
How it Works
- Declaration: When you define a function that needs to access a global resource, you add 'acquires' followed by the resource type after the function signature.
- Compiler Check: The Move compiler uses this information to ensure that the function only accesses the declared resources, preventing unintended access to other global resources.
- Safety: It helps in preventing race conditions and ensures safe concurrent execution of transactions.
Example Usage
public fun read_global_data(addr: address): u64 acquires GlobalData {
borrow_global<GlobalData>(addr).value
}
In this example, the function declares that it will acquire the 'GlobalData' resource from global storage.
Important Notes
- Multiple Resources: A function can acquire multiple resources by listing them after 'acquires', separated by commas.
- Nested Calls: If a function calls another function that acquires a resource, the calling function must also declare that it acquires that resource.
- Compiler Enforcement: The Move compiler will throw an error if a function tries to access a global resource without declaring it with 'acquires'.
By using 'acquires', Move provides a clear and safe way to manage access to global storage, enhancing the security and predictability of smart contracts.