The Aptos Blockchain
by Greg Nazario
The Aptos blockchain is a high-performance, scalable, and secure blockchain platform designed to support a wide range of decentralized applications (dApps) and services. It is built on the Move programming language, which was originally developed for the Diem project by Facebook.
Get started with the Introduction to learn more about the Aptos blockchain or skip ahead to specific topics.
Introduction
The Aptos book is an extension to the source documentation in the Move book. The purpose of the Aptos book is to provide a single resource for design patterns, usage, and other resources for building on Aptos. This should only be providing context for how to build on Aptos, and anything needed for that. The additional benefit is that it can be run entirely offline, for other locations.
Get Started
To use this book, you can skip ahead to different sections for specific topics. However, it is mostly structured so that the reader first learns about the building blocks of Aptos and then builds up to writing Move contracts on top.
It's suggested to learn in the order of chapters of the book. You can also use the search bar to find specific topics or keywords.
Contribution
Please feel free to open a GitHub issue to add more information into each of these sections. All pull requests must:
- Define the section that it is updating
- Provide a concise description of the change
- Call out any missing areas
Getting Started
Let's get you started with the Aptos Blockchain! We'll discuss:
- Installing the Aptos CLI on macOS, Linux, and Windows.
- Hello Blockchain! - a simple contract that writes and reads data on-chain.
- Hello Aptos CLI! - a simple example of using the Aptos CLI to interact with the blockchain.
Installation
The first step is to install the Aptos CLI. The CLI is a command-line interface(CLI) tool that allows you to interact with the Aptos blockchain, including compiling, testing, and deploying Move modules, managing accounts, and reading and writing data to the blockchain.
Note: If you are using an unsupported platform or configuration, or you prefer to build a specific version from source, you can follow the Building from Source guide on the Aptos developer docs.
The following instructions will tell you how to install the latest version of the Aptos CLI. It is highly recommended to always use the latest version of the CLI, as it contains the latest features and bug fixes.
Installing the Aptos CLI with Homebrew on macOS
For macOS, it's recommended to use Homebrew. To install the Aptos CLI on macOS, if you have Homebrew installed, you can use the following command:
brew install aptos
Installing the Aptos CLI on macOS and Linux
To install the Aptos CLI on macOS and Linux, you can use the following command:
curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh
Installing the Aptos CLI on Windows with Winget
For Windows users, you can use the Windows Package Manager (Winget) to install the Aptos CLI. Open a command prompt or PowerShell and run the following command:
winget install aptos.aptos-cli
Installing the Aptos CLI on Windows
To install the Aptos CLI on Windows, you can use the following command in PowerShell:
iwr "https://aptos.dev/scripts/install_cli.ps1" -useb | iex
Troubleshooting
To check whether you have the Aptos CLI installed correctly, open a shell and enter this line:
aptos --version
You should see output similar to the following, with your CLI version.
aptos 7.6.0
If you see this information, you have installed the Aptos CLI successfully! If you don’t see this information, check
that the Aptos CLI is in your %PATH%
system variable as follows.
In Windows CMD, use:
echo %PATH%
In PowerShell, use:
echo $env:Path
In Linux and macOS, use:
echo $PATH
Updating the Aptos CLI
To update the Aptos CLI to the latest version, you can use the same command you used to install it. For example, if you installed the CLI using Homebrew, you can run:
brew upgrade aptos
If you installed the CLI using the curl command, you can run the aptos update command:
aptos update aptos
Alternatively, you can run the installation command again:
curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh
Local Documentation
The Aptos CLI also provides local documentation that you can access by running the following command:
aptos --help
This command will display a list of available commands and options for the Aptos CLI, along with a brief description of each command. You can also access the documentation for a specific command by running:
aptos <command> --help
Text Editors and Integrated Development Environments
This book makes no assumptions about what tools you use to author Move code. Just about any text editor will get the job done! However, many text editors and integrated development environments (IDEs) have built-in support for Move. You can always find a fairly current list of many editors and IDEs.
TODO: add links to the IDEs and editors that support Move.
Hello Blockchain!
Let's start with the simplest example, which shows you how to:
- Build and publish a contract
- Write data on-chain
- Read data on-chain
Example code
Below is the full contract for hello_blockchain.
/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {
use std::error;
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug::print;
/// A resource for a single storage slot, holding a message.
struct MessageHolder has key {
message: String,
}
#[event]
/// Event representing a change in a message, records the old and new messages, and who wrote it.
struct MessageChange has drop, store {
account: address,
from_message: String,
to_message: String,
}
/// The address does not contain a MessageHolder
const E_NO_MESSAGE: u64 = 1;
#[view]
/// Reads the message from storage slot
public fun get_message(addr: address): String acquires MessageHolder {
assert!(exists<MessageHolder>(addr), error::not_found(E_NO_MESSAGE));
MessageHolder[addr].message
}
/// Sets the message to the storage slot
public entry fun set_message(account: signer, message: String) acquires MessageHolder {
let account_addr = signer::address_of(&account);
if (!exists<MessageHolder>(account_addr)) {
move_to(&account, MessageHolder {
message,
})
} else {
let message_holder = &mut MessageHolder[account_addr];
let from_message = message_holder.message;
event::emit(MessageChange {
account: account_addr,
from_message,
to_message: message,
});
message_holder.message = message;
}
}
#[test(account = @0x1)]
fun sender_can_set_message(account: signer) acquires MessageHolder {
let msg: String = string::utf8(b"Running test sender_can_set_message...");
print(&msg);
let addr = signer::address_of(&account);
aptos_framework::account::create_account_for_test(addr);
set_message(account, string::utf8(b"Hello, Blockchain"));
assert!(get_message(addr) == string::utf8(b"Hello, Blockchain"));
}
}
Breakdown
Module
The first 3 lines here, define documentation and the name of the module. Here you can see that the ///
represents a
doc comment. Documentation can be generated from these comments, where ///
describes what's directly below it.
module hello_blockchain::message
represents the name of the address, and the name of the module. hello_blockchain
is what we call a named address. This named address can be passed in at compile time, and determines where the contract
is being deployed. message
is the name of the module. By convention, these are lowercased.
/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {}
Imports
Next, we import some libraries that we will use in our contract. The use
keyword is used to import modules, and are of
the form use <module_address>::<module_name>
. There are three standard library addresses that we can use from Aptos:
std
- The standard library, which contains basic functionality like strings, vectors, and events.aptos_std
- The Aptos standard library, which contains functionality specific to the Aptos blockchain, like string manipulation.aptos_framework
- The Aptos framework, which contains functionality specific to the Aptos framework, like events, objects, accounts and more.
Note that the #[test_only]
attribute is used to indicate that the module is only for testing purposes, and will not be
compiled into the non-test bytecode. This is useful for debugging and testing purposes.
use std::error;
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug::print;
Structs
Next, we define a struct that will hold our message. Structs are structured data that is a collection of other types.
These types can be primitives (e.g. u8
, bool
, address
) or other structs. In this case, the struct is called
MessageHolder
, and it has a single field to hold the message.
/// A resource for a single storage slot, holding a message.
struct MessageHolder has key {
message: String,
}
Events
Next, we define an event that will be emitted when the message is changed. Events are used to record changes to the
blockchain in an easily indexable way. They are similar to events in other programming languages, and can be used to log
changes to the blockchain. In this case, the event is called MessageChange
, and it has three fields: account
, which
is the address of the account that changed the message, from_message
, which is the old message, and to_message
,
which
is the new message. The has drop, store
attributes indicate that the event can be dropped and stored in the
blockchain.
#[event]
/// Event representing a change in a message, records the old and new messages, and who wrote it.
struct MessageChange has drop, store {
account: address,
from_message: String,
to_message: String,
}
Hello Aptos CLI!
TODO: This file is a work in progress. It will be updated as we add more content to the book. It will give you a brief introduction to the Aptos CLI and how to use it to interact with the Aptos blockchain.
Programming a Guessing Game
Guessing Game Contract
Testing the Contract
Deploying the Contract
Interacting with the Contract
Extending the Contract
Conclusion
Common Programming Concepts
Variables
Data Types
Functions
Comments
Control Flow
Understanding Ownership
Ownership Basics
Move Types and Ownership
Transfer and Borrowing
Reference Restrictions
Using Structs to Structure Related Data
Struct Basics
Structs and How They Become Resources
Structs and Abilities
Example Program Using Structs
Struct Method Syntax
Enums and Pattern Matching
Defining Enums
Pattern Matching with Enums
Managing Modules and Packages
Module Basics
Module Imports
Function Scopes
Package Basics
Package Dependencies
Package Publishing
Common Collections
Vectors
Maps
Tables
Error Handling
Aborting Unrecoverable Errors
Handling Recoverable Errors
When to Use Aborts vs. Recoverable Errors
Generics Types
Generic Types Basics
Considerations using GEnerics
How to Write Tests
Writing Unit Tests
Running Unit Tests
Coverage
Formal Verification
Lambda Closures
Defining and Using Closures
Events
Defining and Emitting Events
Listening to Events
Indexing Events
Concurrency
Concurrency Basics
Concurrency and Move Types
Design Patterns
Account Authorization by Signer
Event Emission
Data Models
One of the important concepts of Aptos is flexibility. Aptos provides multiple data models to allow users to pick and choose the model that is best for their use case, or even mix and match them accordingly.
Move on Aptos is built around two base data models. To read more about them, see each page:
- Account Model - Similar to the account model of Ethereum
- Object Model - Similar to the UTXO model of Bitcoin
The Account Model
The Account model of Aptos behaves where each user has their data stored in global storage. Think of this as a giant
mapping between the set of Address
and Resource Name
to a single storage slot.
TODO: Diagram
There are two types of accounts today:
User Accounts
User accounts are the standard accounts that users create to interact with the Aptos blockchain. They are
denoted by the resource 0x1::account::Account
and are used to hold assets, execute transactions, and interact with
smart contracts. They are generated from a signer and are associated with a public key. The public key is then hashed
to create the account address.
Resource Accounts
Resource accounts are accounts that are separate of a user account. The accounts are derived from an
existing account, and can have a SignerCapability
stored in order to sign as the account. Alternatively, the signer
can be rotated to 0x0
preventing anyone from authenticating as an account.
The Object Model
Objects similar to resource accounts, but rather than using a SignerCapability
instead a ExtendRef
can be used to authenticate for the account. These have owners, and always have the resource 0x1::object::Object
stored at its address.
TODO: Diagram
graph subgraph Account 0x1::account::Account end subgraph Object subgraph 0x1::object::ObjectCore Owner end A[0x1::object::ObjectCore] B[0x42::example::Resource] C[0x1234::example2::Resource2] end Owner --> 0x1::account::Account
Data Model Tradeoffs
You probably are asking, when would I use one over the other? Here are some details of advantages and disadvantages of each.
Account Model
The account model is very simple where each address has direct ownership over its resources. The resources at that address can only be added with that signer. Keep in mind, we'll mention both user and resource accounts below.
Account Model Advantages
- User accounts are simple and directly tied to a key.
- Resource accounts are similar to contract addresses in Eth, and can have programmatic access.
- Only the signer associated with the key can write data to the account.
- All resources are indexed by account, and type. Easily accessed automatically in transactions by the signer.
- Creator control over how resources in an account are accessed.
- Ownership based indexing is simple, the account containing the resources is the owner.
Account Model Disadvantages
- Parallelism is not as easy, requires to ensure that multiple accounts don't access a shared resource.
- No programmatic access except for resource accounts.
- No way to get rid of resources in an account, except through the original contract.
Object Model
The object model also has each address has resources owned by the owner of the object. This helps provide more complex ownership models, as well as some tricks for providing composability, and soul-bound resources.
Object Model Advantages
- Parallelism across objects are easy, just create separate objects for parallel tasks.
- Built in ownership.
- Resources are collected easily in a resource group.
- With the resource group, all resources in the group get written to the write set.
- Multiple resources in the resource group only cause a single storage read (less gas).
- Addresses can be randomly generated or derived from the initial owner for instant access.
- Programmatic signer access.
- Composability is easy, NFTs own other NFTs etc.
- Creator control over ownership, transfers, and other pieces.
- Owner can choose to hide the object, allowing wallets or other items to hide it.
Object Model Disadvantages
- For full parallelism, addresses need to be stored off-chain and passed into functions,
- Keeping track of objects can be complex.
- More complex access, does require handling ownership or other access actions.
- Soul-bound objects cannot be removed entirely, indexers need to ignore the resources to make them disappear.
- More complex indexing needed to keep track of object owners and properties (especiallly with ownership chains).
Advanced Topics
Binary Canonical Serialization (BCS)
Binary Canonical Serialization (BCS) is a method that allows for compact and efficient storage. It was invented at Diem for the purposes of a consistent signing mechanism.
Properties
Binary
The serialization method is directly in bytes and is not human-readable. For example, for a String hello
, it would be
represented by the length of the string as a binary encoded uleb-128, followed by the UTF-8 encoded bytes of hello.
e.g. "hello" = 0x0548656C6C6F
This is different than, say, a human-readable format such as JSON which would give
"hello"
Canonical
There is only one canonical way to represent the bytes. This ensures that signing and representation are consistent.
Example:
Let's consider this struct in Move:
module 0x42::example {
struct FunStruct {
a: u8,
b: u8
}
}
In JSON, the struct {"a":1, "b": 2}
can also be represented as {"b":2, "a":1}
. Both are interchangeable so
they are not canonical. In BCS, it would be a pre-defined order, so only one would be the valid representation.
However, in BCS, there is only one valid representation of that, which would be the bytes 0x0102
. 0x0201
is not
canonical, and it would instead be interpreted as {"a":2, "b":1}
.
Non-self describing
The format is not self-describing. This means that deserialization requires knowledge of the shape and how to interpret the bytes. This is in opposition to a type like JSON, which is self-describing.
Example:
Let's consider this struct in Move again:
module 0x42::example {
struct FunStruct {
a: u8,
b: u8
}
}
The first byte will always be interpreted as a
then b
. So, 0x0A00
would be {"a":10, "b":0}
and 0x0A01
would be
{"a":10, "b":1}
. If we flip it to 0x000A
it would be {"a":0, "b":10}
.
Note that this means if I do not know what the shape of the struct is, then I do not know if this is a single u16
, the
above struct, or something else.
Details about different types
BCS Primitives
Here is a list of primitives, and more descriptions below. Keep in mind all numbers are stored in little-endian byte order.
Type | Number of bytes | Description |
---|---|---|
bool | 1 | Boolean value (true or false) |
u8 | 1 | Unsigned 8-bit integer |
u16 | 2 | Unsigned 16-bit integer |
u32 | 4 | Unsigned 32-bit integer |
u64 | 8 | Unsigned 64-bit integer |
u128 | 16 | Unsigned 128-bit integer |
u256 | 32 | Unsigned 256-bit integer |
address | 32 | Aptos Address (32-byte integer) |
uleb128 | 1-32 | Unsigned little-endian base-128 integer |
Bool
A boolean is a single byte. 0x00
represents false
, 0x01
represents true
. All other values are defined as
invalid.
Examples:
Value | BCS Serialized Value |
---|---|
false | 0x00 |
true | 0x01 |
U8
A U8 is an unsigned 8-bit integer (1 byte).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00 |
1 | 0x01 |
16 | 0x0F |
255 | 0xFF |
U16
A U16 is an unsigned 16-bit integer (2 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000 |
1 | 0x0001 |
16 | 0x000F |
255 | 0x00FF |
256 | 0x0100 |
65535 | 0xFFFF |
U32
A U32 is an unsigned 32-bit integer (4 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00000000 |
1 | 0x00000001 |
16 | 0x0000000F |
255 | 0x000000FF |
65535 | 0x0000FFFF |
4294967295 | 0xFFFFFFFF |
U64
A U64 is an unsigned 64-bit integer (8 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000000000000000 |
1 | 0x0000000000000001 |
16 | 0x000000000000000F |
255 | 0x00000000000000FF |
65535 | 0x000000000000FFFF |
4294967295 | 0x00000000FFFFFFFF |
18446744073709551615 | 0xFFFFFFFFFFFFFFFF |
U128
A U128 is an unsigned 128-bit integer (16 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00000000000000000000000000000000 |
1 | 0x00000000000000000000000000000001 |
16 | 0x0000000000000000000000000000000F |
255 | 0x000000000000000000000000000000FF |
65535 | 0x0000000000000000000000000000FFFF |
4294967295 | 0x000000000000000000000000FFFFFFFF |
18446744073709551615 | 0x0000000000000000FFFFFFFFFFFFFFFF |
340282366920938463463374607431768211455 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
U256
A U256 is an unsigned 256-bit integer (32 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000000000000000000000000000000000000000000000000000000000000000 |
1 | 0x0000000000000000000000000000000000000000000000000000000000000001 |
16 | 0x000000000000000000000000000000000000000000000000000000000000000F |
255 | 0x00000000000000000000000000000000000000000000000000000000000000FF |
65535 | 0x000000000000000000000000000000000000000000000000000000000000FFFF |
4294967295 | 0x00000000000000000000000000000000000000000000000000000000FFFFFFFF |
18446744073709551615 | 0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF |
340282366920938463463374607431768211455 | 0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
115792089237316195423570985008687907853269984665640564039457584007913129639935 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
Address
An address is the 32-byte representation of a storage slot on Aptos. It can be used for accounts, objects, and other addressable storage.
Addresses, have special addresses 0x0
-> 0xA
, and then full length addresses. Note, for legacy purposes, addresses
missing 0
's in front, are extended by filling the missing bytes with 0
s.
Examples:
Value | BCS Serialized Value |
---|---|
0x0 | 0x0000000000000000000000000000000000000000000000000000000000000000 |
0x1 | 0x0000000000000000000000000000000000000000000000000000000000000001 |
0xA | 0x000000000000000000000000000000000000000000000000000000000000000A |
0xABCDEF (Legacy shortened address) | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF |
0x0000000000000000000000000000000000000000000000000000000000ABCDEF | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF |
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
Uleb128
A uleb128 is a variable-length integer used mainly for representing sequence lengths. It is very efficient at storing
small numbers, and takes up more space as the number grows. Note that the Aptos specific implementation only supports
representing a u32
in the uleb128. Any value more than the max u32
is considered invalid.
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00 |
1 | 0x01 |
127 | 0x7F |
128 | 0x8001 |
240 | 0xF001 |
255 | 0xFF01 |
65535 | 0xFFFF03 |
16777215 | 0xFFFFFF07 |
4294967295 | 0xFFFFFFFF0F |
Sequences
Sequences are represented as an initial uleb128 followed by a sequence of encoded types. This can include sequences nested inside each other. You can compose them together to make more complex nested vectors
Detailed Example:
The most trivial example is an empty sequence, which is always represented as the zero length byte 0x00
. This is for
any sequence no matter the type.
A more complex example is the vector<u8>
[0, 1, 2]
. We first encode the length, as a uleb128, which is the byte
0x03
. Then, it is followed up by the three individual u8 bytes 0x00
, 0x01
, 0x02
. This gives us an entire byte
array of 0x03000102
.
Examples:
Type | Value | Encoded Value |
---|---|---|
vector<u8> | [] | 0x00 |
vector<u8> | [2] | 0x0102 |
vector<u8> | [2,3,4,5] | 0x0402030405 |
vector<bool> | [true, false] | 0x020100 |
vector<u16> | [65535, 1] | 0x02FFFF0001 |
vector<vector<u8>> | [[], [1], [2,3]] | 0x03000101020203 |
vector<vector<vector<u8>>> | [[[],[1]],[],[[2,3],[4,5]]] | 0x03020001010002020203020405 |
Longer examples (multi-byte uleb128 length):
Type | Value | Encoded Value |
---|---|---|
vector<u8> | [0,1,2,3,...,126,127] | 0x8001000102...FDFEFF |
vector<u32> | [0,1,2,...,4294967293,4294967294,4294967295] | 0xFFFFFFFF0F0000000000000001...FFFFFFFEFFFFFFFFF |
Structs
Structs are represented as an ordered list of bytes. They must always be in this same order. This makes it very simple to always interpret the bytes in a struct.
Detailed example:
Consider the following struct:
module 0x42::example {
struct ExampleStruct {
number: u8,
vec: vector<bool>,
uint16: u16
}
}
We see here that we have mixed types. These types will always be interpreted in that order, and must be canonical.
Here is an example of the struct:
ExampleStruct {
number: 255,
vec: vector[true, false, true],
uint16: 65535
}
This would be encoded as each of the individual encodings in order.
255 = 0xFF
[true, false, true] = 0x03010001
65535 = 0xFFFF
So combined they would be:
0xFF03010001FFFF
Enums
Enums allow for upgradable and different types in a compact representation. They are headed first by a type (in a uleb128), followed by the expected type values.
Example:
Here is an enum in Move, you can see the first value is a struct, the second is a simple value, and the third is a tuple.
module 0x42::example {
enum ExampleStruct {
T1 {
number: u8,
vec: vector<bool>,
uint16: u16
},
T2,
T3(u8, bool)
}
}
Let's start with the first type:
ExampleStruct::T1 {
number: 1,
vec: [true, false, true]
uint16: 65535
}
This would first start with the initial uleb128 representing the type, then followed by the bytes. In this case, it is
the first in the enum, so it will be represented as enum 0
. All together it is represented by: 0x000103010001FFFF
.
For the second type, it's simply just represented as the uleb128 representing the type for value 1
:
ExampleStruct::T2 {} = 0x01
For the third type, it's represented as the uleb128 representing the type for value 2
followed by the tuple:
ExampleStruct::T2(3,true) = 0x020301
Strings
Strings are a special case. They are represented as a vector<u8>
, but only UTF-8
valid characters are allowed. This
means, it would look exactly the same as a vector. Note, that the sequence length is the number of bytes, not the
number of characters.
For example the string: ❤️12345
= 0x0BE29DA4EFB88F3132333435
Examples:
Value | Encoded Value |
---|---|
A | 0x0141 |
hello | 0x0568656C6C6f |
goodbye | 0x07676F6F64627965 |
❤️ | 0x06e29da4efb88f |
💻 | 0x03E29C8D |
Options
Options are a special case of enums. It is simply either a 0
for a None
option and a 1
plus the value
for a Some
option.
Examples:
Type | Value | Encoded Value |
---|---|---|
Option | None | 0x00 |
Option | Some(false) | 0x0100 |
Option | Some(true) | 0x0101 |
Option<vector<u16>> | None | 0x00 |
Option<vector<u16>> | Some([1,65535)) | 0x01020001FFFF |
Storage
Aptos currently uses RocksDB for on-chain storage. The data is stored in mainly addressable storage on-chain, but the Digital Assets Standard uses off-chain storage for image and other assets.
Types of storage
On-chain Storage
On-chain Rocks DB uses a concept of storage slots. The storage slots are accessible storage, and each slot has an associated cost with it. The slots are accessible by a hash of given inputs.
There are a few types:
- Resources
- Resource Groups
- Tables
- Vectors
- BigVectors
- SimpleMap
- OrderedMap
- BigOrderedMap
Resources
Resource storage a single slot is the combination of address and resource name. This combination gives a single slot, which storing a lot of data in that resource, can be efficient.
TODO: Diagram
Keep in mind that the storage deposit associated with a resource is a single slot per resource.
Resource Groups
As specified in AIP-9, resource groups are a collection of resources in a single storage slot. These are stored as a b-tree, in the storage slot, which allows for more efficient packing of data (fewer slots). This is a slight tradeoff where, if accessing multiple resources together often, will be much more efficient, but all writes send all resources in the group to the output writeset. There is also slight cost to navigate the b-tree.
TODO: Diagram
The most common usage for resource groups is for the 0x1::object::ObjectGroup
group for objects.
Tables
Tables are hash addressable storage based on a handle stored in a resource. Each item in a table is a single storage slot, but with the advantage that has less execution cost associated. Additionally, each storage slot can be parallelized separately. Note that by far tables are the most expensive, as you need to store both the slot for the handle and the slot for each individual table item. The basic table handle cannot be deleted, but the table items can be. The cost of the table handle's slot cannot be recovered via storage refund.
TODO: Diagram
Note that there is no indexed tracking of which table items are filled or not and how many there are, this must be done off-chain or with a different table variant.
There are other variants of the table with different tradeoffs:
- Table with length
- Smart Table
Table With Length
Table with length is exactly the same as a table, but with a length value. Keep in mind that table with length can be deleted fully including the table handle. However, table with length is not parallelizable on creation or deletion of table items, because every transaction increments or decrements the length.
TODO: Diagram
Smart Table
Smart table uses buckets to lower the number of storage slots used. It keeps track of the length, and it buckets items into vectors. It is additionally iterable over the course of the whole table.
TODO: Diagram
Note: there is a possible DDoS vector if people can create keys that end up in a single storage item.
Vector
Vectors are sequences of
Off-Chain Storage
All URLs stored on Aptos can be of the following:
- An HTTP or HTTPS URL e.g. https://mydomain.com/image.png
- An IPFS URL e.g. ipfs://hash
TODO: Followup with more info / examples
Indexing
Off-chain indexing is also very common, see https://aptos.dev for more info.
TODO: Link tutorials and other information.
Gas
Gas is used to measure the amount of execution, storage, and IO used for every transaction. This is to provide fairness and ensure the network runs smoothly and with high performance. Gas cost for a transaction has three parts:
- The gas used - which is the amount of units used to execute the transaction (execution, storage, and IO).
- The gas unit price (also sometimes called price per unit gas) - which is the amount the fee payer chose to pay to prioritize the transaction.
- The storage refund - which is based on the number of storage slots deleted.
The total gas cost (fee or refund) is calculated as: TODO: Add LaTeX to make this nicer
(gas used x gas unit price) + storage refund = total gas
Keep in mind, the storage refund can be greater than the other side, so you can actually gain gas in a transaction by freeing storage slots.
How is gas used calculated?
Gas used is calculated by three parts:
- The number of execution units, which vary based on the operations taken.
- The number of IO units, which vary based on which storage slots are read or written.
- The storage deposit, which is the cost for each new storage slot created. Storage deposit is returned to users.
Each one of these has an individual upper bound, so keep that in mind if you have a task that uses any one of these heavily.
Execution
Execution gas is measured by the amount of work that an operation does on-chain. Keep in mind that this includes things like:
- Iterating over items in a table
- Performing cryptographic operations
- Unpacking input arguments
- Mathematical operations
- Control flow operations
IO
IO operations are the amount of reads and writes done to existing storage slots (or storage slots after they are created). This includes things like:
- Writing to existing storage
- Adding values to a vector and saving it in storage
- Deleting values from a vector and saving it in storage
- Reading existing storage slots
Storage Deposit / Refund
Storage deposit (and subsequent refund) are based on the number of storage slots created or deleted. Each storage slot has a cost associated with it, which is deposited to encourage freeing of storage slots later. The storage slots that are freed will then refund the fee payer of the transaction, possibly even making their transaction be free or even pay them for the transaction.
TODO: More details
Configuring Gas
There are only 2 knobs you can turn today on Aptos to configure gas cost for a transaction.
- Max gas amount - The number of gas units you are willing to spend on a transaction.
- Gas unit price - The number of octas (APT*10^-8) from a minimum of 100.
Max gas amount
If a max gas amount is too low for a transaction to complete, OUT_OF_GAS
will be returned, and the transaction will
abort. This means that nothing will happen in the transaction, but you will still pay the gas. Setting this to a
reasonable bound prevents you from spending too much on a single transaction.
Range: 2 - ??? (TODO: Put number or how to get it from gas config)
If you do not have enough APT to fulfill the gas deposit (max gas amount * gas unit price), you will get an error of
INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE
. This means you will need more APT in your account, or to adjust one of these
two values.
Gas unit price
Gas unit price is the amount you're willing to pay per gas unit on a transaction. Higher values are prioritized over lower values. When choosing a gas unit price, keep in mind that your account needs enough APT to pay for the entire max gas amount times the gas unit price.
Range: 100 - ??? (TOOD: put number or how to get it from gas config)
Gas Config
TODO: Explain how to get it directly from on-chain.
Standard Libraries
Aptos provides multiple standard libraries at the given addresses:
- 0x1
- 0x3
- AptosToken - Legacy NFT standard (not suggested for any future usage)
- 0x4
- AptosTokenObjects - Digital Assets -> New NFT and semi-fungible token standard
Standards
These standards are built into the 0x1 and 0x4 addresses:
- Fungible Tokens
- Non-fungible and Semi-fungible Tokens
Additional Libraries
These are from third parties, and can be used as well.
TODO
Coin (Legacy)
Coin is the original standard for fungible tokens on Aptos. It is being replaced with the fungible asset standard, which allows for much more flexibility and more fine-grained control over the fungible tokens. Note that APT is a Coin being migrated to the fungible asset.
TODO: Simple example
Migrating coins to Fungible asset
TODO
Fungible Assets
The fungible asset standard is a fungible token standard built for flexibility and control over supplies and their properties. Note that this is built upon the object model.
Dispatchable Fungible Assets
TODO: More details
Digital Assets
The digital asset standard is a token standard for both non-fungible and semi-fungible tokens. It's important to note that combining the fungible asset standard and the digital asset standard create semi-fungible tokens. Note that this is built upon the object model.
Move Stdlib
TODO: Link source, possibly give a high level overview
Aptos Stdlib
TODO: Link source, possibly give a high level overview
Aptos Framework
TODO: Link source, possibly give a high level overview
graph A[MoveStdLib] --> B[AptosStdLib] B --> C[AptosFramework] C --> D[AptosToken] C --> E[AptosTokenObjects]
Aptos by Example
A list of examples illustrating how different concepts and features work on Aptos through copyable examples. It provides source code and associated explanations.
TODO: Add an outline of features and the examples associated
- Hello Blockchain - The simplest contract
- Error Handling - How to handle errors
1 - Hello Blockchain
Let's start with the simplest example, which shows you how to:
- Build and publish a contract
- Write data on-chain
- Read data on-chain
Example code
Below is the full contract for hello_blockchain.
/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {
use std::error;
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug::print;
/// A resource for a single storage slot, holding a message.
struct MessageHolder has key {
message: String,
}
#[event]
/// Event representing a change in a message, records the old and new messages, and who wrote it.
struct MessageChange has drop, store {
account: address,
from_message: String,
to_message: String,
}
/// The address does not contain a MessageHolder
const E_NO_MESSAGE: u64 = 1;
#[view]
/// Reads the message from storage slot
public fun get_message(addr: address): String acquires MessageHolder {
assert!(exists<MessageHolder>(addr), error::not_found(E_NO_MESSAGE));
MessageHolder[addr].message
}
/// Sets the message to the storage slot
public entry fun set_message(account: signer, message: String) acquires MessageHolder {
let account_addr = signer::address_of(&account);
if (!exists<MessageHolder>(account_addr)) {
move_to(&account, MessageHolder {
message,
})
} else {
let message_holder = &mut MessageHolder[account_addr];
let from_message = message_holder.message;
event::emit(MessageChange {
account: account_addr,
from_message,
to_message: message,
});
message_holder.message = message;
}
}
#[test(account = @0x1)]
fun sender_can_set_message(account: signer) acquires MessageHolder {
let msg: String = string::utf8(b"Running test sender_can_set_message...");
print(&msg);
let addr = signer::address_of(&account);
aptos_framework::account::create_account_for_test(addr);
set_message(account, string::utf8(b"Hello, Blockchain"));
assert!(get_message(addr) == string::utf8(b"Hello, Blockchain"));
}
}
Breakdown
Module
The first 3 lines here, define documentation and the name of the module. Here you can see that the ///
represents a
doc comment. Documentation can be generated from these comments, where ///
describes what's directly below it.
module hello_blockchain::message
represents the name of the address, and the name of the module. hello_blockchain
is what we call a named address. This named address can be passed in at compile time, and determines where the contract
is being deployed. message
is the name of the module. By convention, these are lowercased.
/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {}
Imports
Next, we import some libraries that we will use in our contract. The use
keyword is used to import modules, and are of
the form use <module_address>::<module_name>
. There are three standard library addresses that we can use from Aptos:
std
- The standard library, which contains basic functionality like strings, vectors, and events.aptos_std
- The Aptos standard library, which contains functionality specific to the Aptos blockchain, like string manipulation.aptos_framework
- The Aptos framework, which contains functionality specific to the Aptos framework, like events, objects, accounts and more.
Note that the #[test_only]
attribute is used to indicate that the module is only for testing purposes, and will not be
compiled into the non-test bytecode. This is useful for debugging and testing purposes.
use std::error;
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug::print;
Structs
Next, we define a struct that will hold our message. Structs are structured data that is a collection of other types.
These types can be primitives (e.g. u8
, bool
, address
) or other structs. In this case, the struct is called
MessageHolder
, and it has a single field to hold the message.
/// A resource for a single storage slot, holding a message.
struct MessageHolder has key {
message: String,
}
Events
Next, we define an event that will be emitted when the message is changed. Events are used to record changes to the
blockchain in an easily indexable way. They are similar to events in other programming languages, and can be used to log
changes to the blockchain. In this case, the event is called MessageChange
, and it has three fields: account
, which
is the address of the account that changed the message, from_message
, which is the old message, and to_message
,
which
is the new message. The has drop, store
attributes indicate that the event can be dropped and stored in the
blockchain.
#[event]
/// Event representing a change in a message, records the old and new messages, and who wrote it.
struct MessageChange has drop, store {
account: address,
from_message: String,
to_message: String,
}
Learning Resources
Appendix
CLI Installation Methods
macOS
Homebrew
Installation
To install the Aptos CLI on macOS, you can use Homebrew, a popular package manager. Open your terminal and run the following command:
brew install aptos
Upgrading
To upgrade the Aptos CLI using Homebrew, run:
brew upgrade aptos
Script Installation
Installation
To install the Aptos CLI using a script, you can use the following command in your terminal:
curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh
Upgrading
To upgrade the Aptos CLI using the script, you can run the same command again:
curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh
Linux
Script Installation
Installation
To install the Aptos CLI on Linux, you can use the following command in your terminal:
curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh
Upgrading
To upgrade the Aptos CLI using the script, you can use the CLI:
aptos update aptos
Or you can run the installation command again:
curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh
If you are getting
Illegal instruction
errors when running the CLI, it may be due to your CPU not supporting SIMD instructions. Specifically for older non-SIMD processors or Ubuntu x86_64 docker containers on ARM Macs, you may need to run the following command instead to skip SIMD instructions:
curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh -s -- --generic-linux
Windows
Winget
Installation
To install the Aptos CLI on Windows using Winget, open a command prompt or PowerShell and run the following command:
winget install aptos.aptos-cli
Upgrading
To upgrade the Aptos CLI using Winget, run:
winget upgrade aptos.aptos-cli
Chocolatey
Installation
To install the Aptos CLI on Windows using Chocolatey, open a command prompt or PowerShell with administrative privileges and run:
choco install aptos-cli
Upgrading
To upgrade the Aptos CLI using Chocolatey, run:
choco upgrade aptos-cli
Script Installation
Installation
To install the Aptos CLI on Windows using a script, you can use the following command in PowerShell:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser;
iwr https://aptos.dev/scripts/install_cli.ps1 | iex
Upgrading
To upgrade the Aptos CLI using the script, you can use the CLI:
aptos update aptos
Or you can run the installation command again:
Invoke-WebRequest -Uri "https://aptos.dev/scripts/install_cli.ps1" -OutFile "install_cli.ps1"; .\install_cli.ps1