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:

  1. Define the section that it is updating
  2. Provide a concise description of the change
  3. Call out any missing areas

Getting Started

Let's get you started with the Aptos Blockchain! We'll discuss:

  1. Installing the Aptos CLI on macOS, Linux, and Windows.
  2. Hello Blockchain! - a simple contract that writes and reads data on-chain.
  3. 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:

  1. Build and publish a contract
  2. Write data on-chain
  3. 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:

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.

TypeNumber of bytesDescription
bool1Boolean value (true or false)
u81Unsigned 8-bit integer
u162Unsigned 16-bit integer
u324Unsigned 32-bit integer
u648Unsigned 64-bit integer
u12816Unsigned 128-bit integer
u25632Unsigned 256-bit integer
address32Aptos Address (32-byte integer)
uleb1281-32Unsigned 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:

ValueBCS Serialized Value
false0x00
true0x01

U8

A U8 is an unsigned 8-bit integer (1 byte).

Examples:

ValueBCS Serialized Value
00x00
10x01
160x0F
2550xFF

U16

A U16 is an unsigned 16-bit integer (2 bytes).

Examples:

ValueBCS Serialized Value
00x0000
10x0001
160x000F
2550x00FF
2560x0100
655350xFFFF

U32

A U32 is an unsigned 32-bit integer (4 bytes).

Examples:

ValueBCS Serialized Value
00x00000000
10x00000001
160x0000000F
2550x000000FF
655350x0000FFFF
42949672950xFFFFFFFF

U64

A U64 is an unsigned 64-bit integer (8 bytes).

Examples:

ValueBCS Serialized Value
00x0000000000000000
10x0000000000000001
160x000000000000000F
2550x00000000000000FF
655350x000000000000FFFF
42949672950x00000000FFFFFFFF
184467440737095516150xFFFFFFFFFFFFFFFF

U128

A U128 is an unsigned 128-bit integer (16 bytes).

Examples:

ValueBCS Serialized Value
00x00000000000000000000000000000000
10x00000000000000000000000000000001
160x0000000000000000000000000000000F
2550x000000000000000000000000000000FF
655350x0000000000000000000000000000FFFF
42949672950x000000000000000000000000FFFFFFFF
184467440737095516150x0000000000000000FFFFFFFFFFFFFFFF
3402823669209384634633746074317682114550xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

U256

A U256 is an unsigned 256-bit integer (32 bytes).

Examples:

ValueBCS Serialized Value
00x0000000000000000000000000000000000000000000000000000000000000000
10x0000000000000000000000000000000000000000000000000000000000000001
160x000000000000000000000000000000000000000000000000000000000000000F
2550x00000000000000000000000000000000000000000000000000000000000000FF
655350x000000000000000000000000000000000000000000000000000000000000FFFF
42949672950x00000000000000000000000000000000000000000000000000000000FFFFFFFF
184467440737095516150x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF
3402823669209384634633746074317682114550x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
1157920892373161954235709850086879078532699846656405640394575840079131296399350xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

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 0s.

Examples:

ValueBCS Serialized Value
0x00x0000000000000000000000000000000000000000000000000000000000000000
0x10x0000000000000000000000000000000000000000000000000000000000000001
0xA0x000000000000000000000000000000000000000000000000000000000000000A
0xABCDEF (Legacy shortened address)0x0000000000000000000000000000000000000000000000000000000000ABCDEF
0x0000000000000000000000000000000000000000000000000000000000ABCDEF0x0000000000000000000000000000000000000000000000000000000000ABCDEF
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

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:

ValueBCS Serialized Value
00x00
10x01
1270x7F
1280x8001
2400xF001
2550xFF01
655350xFFFF03
167772150xFFFFFF07
42949672950xFFFFFFFF0F

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:

TypeValueEncoded 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):

TypeValueEncoded 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:

ValueEncoded Value
A0x0141
hello0x0568656C6C6f
goodbye0x07676F6F64627965
❤️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:

TypeValueEncoded Value
OptionNone0x00
OptionSome(false)0x0100
OptionSome(true)0x0101
Option<vector<u16>>None0x00
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:

  1. The number of execution units, which vary based on the operations taken.
  2. The number of IO units, which vary based on which storage slots are read or written.
  3. 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:

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

  1. Hello Blockchain - The simplest contract
  2. Error Handling - How to handle errors

1 - Hello Blockchain

Let's start with the simplest example, which shows you how to:

  1. Build and publish a contract
  2. Write data on-chain
  3. 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

Packaging status

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