Introduction

Welcome to Anoma's docs!

About Anoma

Anoma is a sovereign, proof-of-stake blockchain protocol that enables private, asset-agnostic cash and private bartering among any number of parties. To learn more about the protocol, we recommend the following resources:

Anoma's current testnet: Feigenbaum

Feigenbaum is the name of Anoma's first public testnet. Find feigenbaum on Github.

⚠️ Here lay dragons: this codebase is still experimental, try at your own risk!

About the documentation

The three main sections of this book are:

  • User Guide: explains basic concepts and interactions
  • Exploration: documents the process of exploring the design and implementation space for Anoma
  • Specifications: implementation independent technical specifications

The source

This book is written using mdBook with mdbook-mermaid for diagrams, it currently lives in the Anoma repo.

To get started quickly, in the docs directory one can:

# Install dependencies
make dev-deps

# This will open the book in your default browser and rebuild on changes
make serve

The mermaid diagrams docs can be found at https://mermaid-js.github.io/mermaid.

Contributions to the contents and the structure of this book (nothing is set in stone) should be made via pull requests. Code changes that diverge from the spec should also update this book.

User Guide

Welcome to Anoma user guide!

This guide is intended to help you find how to install, operate and interact with the Anoma ledger and intent gossip nodes, the matchmaker, the client and the wallet.

💾 Install Anoma

There's a single command to build and install Anoma executables from source (the node, the client and the wallet). This command will also verify if a compatible version of Tendermint is available, and if not, attempt to install it. Note that currently at least 16GB RAM is needed to build from source.

make install

Getting started

This guide assumes that the Anoma binaries are installed and available on path. These are:

  • anoma: The main binary that can be used to interact with all the components of Anoma
  • anoman: The ledger and intent gossiper node
  • anomac: The client
  • anomaw: The wallet

The main binary anoma has sub-commands for all of the other binaries:

  • anoma client = anomac
  • anoma node = anoman
  • anoma wallet = anomaw

To explore the command-line interface, add --help argument at any sub-command level to find out any possible sub-commands and/or arguments.

To configure your node for a given chain ID, set the $CHAIN_ID and run:

anoma client utils join-network --chain-id=$CHAIN_ID

The Anoma Wallet

The Anoma wallet allows you to store and use addresses and keys by their alias.

The wallet's state is stored under .anoma/{chain_id}/wallet.toml (with the default --base-dir), which will be created for you if it doesn't already exist when you run any command that accesses the wallet. A newly created wallet will be pre-loaded with some default addresses.

For the ledger and intent gossip commands that use keys and addresses, you can enter their aliases as as defined in the wallet (case-sensitive).

By default, keys are stored encrypted (unless the --unsafe-dont-encrypt flag is used). Currently, the Anoma client can load the password via:

  • file, by exporting an ENV variable called ANOMA_WALLET_PASSWORD_FILE with value containing the path to a file containing the password.
  • env variable, by exporting a ENV variable called ANOMA_WALLET_PASSWORD with value of the actual password.
  • stdin, the client will prompt for a password.

🔐 Keys

For cryptographic signatures, we currently support ed25519 keys. More will be added in future.

To manage keys, various sub-commands are available, see the commands --help:

anoma wallet key

List all known keys:

anoma wallet key list

Generate a new key:

anoma wallet key gen --alias my-key

Note that this will also save an implicit address derived from this public key under the same alias. More about addresses below. This command has the same effect as address gen.

📇 Addresses

All accounts in the Anoma ledger have a unique address, exactly one validity predicate and optionally any additional data in its dynamic storage sub-space.

There are currently 3 types of account addresses:

  • Established: Used for accounts that allow the deployment of custom validation logic. These must be created on-chain via a transaction (see the Ledger guide). The address is generated on-chain and is not known until the transaction is applied.
  • Implicit, not yet fully supported in the ledger: Derived from a key, which can be used to authorize certain transactions from the account. They can be used as recipients of transactions without even when the account has not been used on-chain before.
  • Internal: Special internal accounts, such as protocol parameters account, PoS and IBC

To manage addresses, similar to keys, various sub-commands are available:

anoma wallet address

List all known addresses:

anoma wallet address list

Generate a new implicit address:

anoma wallet address gen --alias my-account

Note that this will also generate and save a key from which the address derived and save it under the same alias. Thus, this command has the same effect as key gen.

The Anoma Ledger

To start a local Anoma ledger node, run:

anoma ledger

The node will attempt to connect to the persistent validator nodes and other peers in the network, and synchronize to the latest block.

By default, the ledger will store its configuration and state in the .anoma directory relative to the current working directory. You can use the --base-dir CLI global argument or ANOMA_BASE_DIR environment variable to change it.

If not found on start up, the ledger will generate a configuration file at .anoma/config.toml.

The ledger also needs access to the built WASM files that are used in the genesis block. These files are included in release and shouldn't be modified, otherwise your node will fail with a consensus error on the genesis block. By default, these are expected to be in the wasm directory, relative to the current working directory. This can also be set with the --wasm-dir CLI global argument, ANOMA_WASM_DIR environment variable or the configuration file.

📝 Initialize an account

If you already have a key in your wallet, you can skip this step and use it in the following commands. Otherwise, generate a new key now:

anoma wallet key gen --alias my-key

Then send a transaction to initialize the account and save its address with the alias my-new-acc. The my-key public key will be written into the account's storage for authorizing future transactions. We also sign this transaction with my-key.

anoma client init-account \
  --alias my-new-acc \
  --public-key my-key \
  --source my-key

Once this transaction has been applied, the client will automatically see the new address created by the transaction and add it to your wallet with the chosen alias my-new-acc.

By default, this command will use the prebuilt user validity predicate (from the vp_user source). You can supply a different validity predicate with the --code-path argument. We'll come back to this topic and cover how to write and deploy custom validity predicates in the custom validity predicates section.

💸 Token transactions and queries

In Anoma, tokens are implemented as accounts with a token validity predicate. It checks that its total supply is preserved in any transaction that uses this token. Your wallet will be pre-loaded with some token addresses that are initialized in the genesis block.

You can see the tokens addresses known by the client when you query all tokens balances:

anoma client balance

XAN is Anoma's native token. To obtain some tokens in a testnet, there is a special "faucet" account that allows anyone to withdraw up to 1000 of any token for a single transaction. You can find the address of this account in your wallet. To get some tokens from the faucet account:

anoma client transfer \
  --source faucet \
  --target my-new-acc \
  --signer my-new-acc \
  --token XAN \
  --amount 1000

Note that because you don't have the key to sign a transfer from the faucet account, in the command above, we set the --signer explicitly to your account's address.

To submit a regular token transfer from your account to the validator-1 address:

anoma client transfer \
  --source my-new-acc \
  --target validator-1 \
  --token XAN \
  --amount 10

This command will attempt to find and use the key of the source address to sign the transaction.

To query token balances for a specific token and/or owner:

anoma client balance --token XAN --owner my-new-acc

Note that for any client command that submits a transaction (init-account, transfer, tx, update and PoS transactions), you can use the --dry-run flag to simulate the transaction being applied in the block, to see what its result would be.

🔏 Interacting with the Proof-of-Stake system

The Anoma Proof of Stake system uses the XAN token as the staking token. It features delegation to any number of validators and customizable validator validity predicates.

The PoS system is implemented as an account with a validity predicate that governs the rules of the system. You can find its address in your wallet:

anoma wallet address find --alias PoS

The system relies on the concept of epochs. An epoch is a range of consecutive blocks identified by consecutive natural numbers. Each epoch lasts a minimum duration and includes a minimum number of blocks since the beginning of the last epoch. These are defined by protocol parameters.

To query the current epoch:

anoma client epoch

You can delegate to any number of validators at any time. When you delegate tokens, the delegation won't count towards the validator's stake (which in turn determines its voting power) until the beginning of epoch n + 2 in the current epoch n (the literal 2 is set by PoS parameter pipeline_len). The delegated amount of tokens will be deducted from your account immediately, and will be credited to the PoS system's account.

To submit a delegation that bonds tokens from the source address to a validator with alias validator-1:

anoma client bond \
  --source my-new-acc \
  --validator validator-1 \
  --amount 12.34

You can query your delegations:

anoma client bonds --owner my-new-acc

The result of this query will inform the epoch from which your delegations will be active.

Because the PoS system is just an account, you can query its balance, which is the sum of all staked tokens:

anoma client balance --owner PoS

Should a validator exhibit punishable behavior, the delegations towards this validator are also liable for slashing. Only the delegations that were active in the epoch in which the fault occurred will be slashed by the slash rate of the fault type. If any of your delegations have been slashed, this will be displayed in the bonds query. You can also find all the slashes applied with:

anoma client slashes

While your tokens are being delegated, they are locked-in the PoS system and hence are not liquid until you withdraw them. To do that, you first need to send a transaction to “unbond” your tokens. You can unbond any amount, up to the sum of all your delegations to the given validator, even before they become active.

To submit an unbonding of a delegation of tokens from a source address to the validator:

anoma client unbond \
  --source my-new-acc \
  --validator validator-1 \
  --amount 1.2

When you unbond tokens, you won't be able to withdraw them immediately. Instead, tokens unbonded in the epoch n will be withdrawable starting from the epoch n + 6 (the literal 6 is set by PoS parameter unbonding_len). After you unbond some tokens, you will be able to see when you can withdraw them via bonds query:

anoma client bonds --owner my-new-acc

When the chain reaches the epoch in which you can withdraw the tokens (or anytime after), you can submit a withdrawal of unbonded delegation of tokens back to your account:

anoma client withdraw \
  --source my-new-acc \
  --validator validator-1

Upon success, the withdrawn tokens will be credited back your account and debited from the PoS system.

To see all validators and their voting power, you can query:

anoma client voting-power

With this command, you can specify --epoch to find the voting powers at some future epoch. Note that only the voting powers for the current and the next epoch are final.

📒 PoS Validators

To register a new validator account, run:

anoma client init-validator \
  --alias my-validator \
  --source my-new-acc

This command will generate the keys required for running a validator:

  • Consensus key, which is used in signing blocks in Tendermint.
  • Validator account key for signing transactions on the validator account, such as token self-bonding, unbonding and withdrawal, validator keys, validity predicate, state and metadata updates.
  • Staking reward account key for signing transactions on the staking reward accounts. More on this account below.

Then, it submits a transaction to the ledger that generates two new accounts with established addresses:

  • A validator account with the main validator address, which can be used to receive new delegations
  • A staking reward account, which will receive rewards for participation in the PoS system. In the future, the validity predicate of this account will be able to control how the rewards are to be distributed to the validator's delegators. Staking rewards are not yet implemented.

These keys and aliases of the addresses will be saved in your wallet. Your local ledger node will also be setup to run this validator, you just have to shut it down with e.g. Ctrl + C, then start it again with the same command:

anoma ledger

The ledger will then use the validator consensus key to sign blocks, should your validator account acquire enough voting power to be included in the active validator set. The size of the active validator set is limited to 128 (the limit is set by the PoS max_validator_slots parameter).

Note that the balance of XAN tokens that is in your validator account does not count towards your validator's stake and voting power:

anoma client balance --owner my-validator --token XAN

That is, the balance of your account's address is a regular liquid balance that you can transfer using your validator account key, depending on the rules of the validator account's validity predicate. The default validity predicate allows you to transfer it with a signed transaction and/or stake it in the PoS system.

You can submit a self-bonding transaction of tokens from a validator account to the PoS system with:

anoma client bond \
  --validator my-validator \
  --amount 3.3

A validator's voting power is determined by the sum of all their active self-bonds and delegations of tokens, with slashes applied, if any, divided by 1000 (PoS votes_per_token parameter, with the current value set to 10‱ in parts per ten thousand).

The same rules apply to delegations. When you self-bond tokens, the bonded amount won't count towards your validator's stake (which in turn determines your power) until the beginning of epoch n + 2 in the current epoch n. The bonded amount of tokens will be deducted from the validator's account immediately and will be credited to the PoS system's account.

While your tokens are being self-bonded, they are locked-in the PoS system and hence are not liquid until you withdraw them. To do that, you first need to send a transaction to “unbond” your tokens. You can unbond any amount, up to the sum of all your self-bonds, even before they become active.

To submit an unbonding of self-bonded tokens from your validator:

anoma client unbond \
  --validator my-validator \
  --amount 0.3

Again, when you unbond tokens, you won't be able to withdraw them immediately. Instead, tokens unbonded in the epoch n will be withdrawable starting from the epoch n + 6. After you unbond some tokens, you will be able to see when you can withdraw them via bonds query:

anoma client bonds --validator my-validator

When the chain reaches the epoch in which you can withdraw the tokens (or anytime after), you can submit a withdrawal of unbonded tokens back to your validator account:

anoma client withdraw --validator my-validator

Customize accounts and transactions

On this page, we'll cover how to tailor your account(s) to your use-case with custom-made validity predicates and transactions.

We currently only support Rust for custom validity predicates and transactions via WASM, but expect many more options to be available in the future!

👩🏽‍🏫 Anoma accounts primer

Instead of the common smart contract design, in Anoma, all the accounts follow the same basic principles. Each account has exactly one validity predicate. Any transaction that attempts to make some storage modifications will trigger validity predicates of each account whose storage has been modified by it. Validity predicates are stateless functions that decide if an account accepts the transaction.

Every account also has its dedicated key-value storage. The ledger encodes agnostic, it allows you to use the encoding of your preference for the storage values. Internally and for the pre-built validity predicates and transactions, we use Borsh, which allows you to simply derive the encoding from your data types. The storage keys are / separated path segments, where the first segment is always the address of the account to which the storage key belongs. In storage keys, addresses use a reserved prefix #.

To illustrate with an example storage key used for fungible tokens (with addresses shortened for clarity), let's say:

  • XAN token's address is atest1v4ehgw36x3prs...

  • A user Bertha has address atest1v4ehgw36xvcyyv...

  • Then, the balance of Bertha's XAN tokens is stored in the XAN account, with the storage key comprised of #{token}/balance/#{owner}, i.e.:

    #atest1v4ehgw36x3prs.../balance/#atest1v4ehgw36xvcyy...
    

Any transaction can attempt to make changes to the storage of any account(s). Only if all the involved accounts accept, the transaction will it be committed. Otherwise, the transaction is rejected and its modifications discarded.

This approach allows multiparty transactions to be applied atomically, without any a priority coordination. It also gives accounts complete and fine-grained control over how they can be used in transactions in themselves and in relation to other accounts.

In fact, most of the functionality in the Anoma ledger is being built leveraging the simplicity and flexibility of this account system, from a simple fungible token to more complex accounts that integrate the Inter-blockchain Communication protocol and the Proof of Stake system.

☑ Validity predicates

A custom validity predicates can be built from scratch using vp_template (from root directory wasm/vp_template), which is Rust code compiled to WASM. Consult its README.md to find out more.

You can also check out the pre-built validity predicates' source code in the wasm/wasm_source, where each sub-module that begins with vp_ implements a validity predicate. For example the vp_user is the default validity predicate used for established accounts (created with init-account command).

A validity predicate's must contain the following function:


#![allow(unused)]
fn main() {
use anoma_vm_env::vp_prelude::*;

#[validity_predicate]
fn validate_tx(
    // The data attached to the transaction
    tx_data: Vec<u8>,
    // The address of the account where this validity predicate is used
    addr: Address,
    // The storage keys that were modified by the transaction
    keys_changed: HashSet<storage::Key>,
    // The addresses of all the accounts that are verifying the current 
    // transaction
    verifiers: HashSet<Address>,
) -> bool {
  // Returning `true` allows any key change
  true
}
}

You can think of it as its main function. When this VP is deployed to an account, this function will be called for every transaction that:

  • Modifies a storage key that contains the account's address to which the validity predicate belongs
  • Inserts the account's address into the verifier set with tx_prelude::insert_verifiers function

Inside the validity predicate function, you can read any storage value with the functions provided in the vp_prelude from the storage prior to the transaction (functions with name suffix _pre) and from the storage state after the transaction is applied (suffixed with _post).

To find out about the host interface available in a validity predicate, please check out Rust docs for vp_prelude.

To compile the validity predicate's code from the template:

make -C wasm/vp_template

This will output a WASM file that can be found in wasm/vp_template/target/wasm32-unknown-unknown/release/vp_template.wasm.

You can, for example, copy it into your wasm directory (the default directory used by the ledger's node and the client, which can be changed with --wasm-dir global argument or ANOMA_WASM_DIR):

cp \
  wasm/vp_template/target/wasm32-unknown-unknown/release/vp_template.wasm \
  wasm/my_vp.wasm

To submit a transaction that updates an account's validity predicate:

anoma client update --address my-new-acc --code-path my_vp.wasm

📩 Custom transactions

A transaction must contain a WASM code that can perform arbitrary storage changes. It can also contain arbitrary data, which will be passed onto the transaction and validity predicates when the transaction is being applied.

A custom transaction can be built from scratch using tx_template (from root directory wasm/tx_template), which is Rust code compiled to WASM. Consult its README.md to find out more.

For some inspiration, check out the pre-built transactions source code in the wasm/wasm_source, where each sub-module that begins with tx_ implements a transaction.

A transaction code must contain the following function, which will be called when the transaction is being applied:


#![allow(unused)]
fn main() {
use anoma_vm_env::tx_prelude::*;

#[transaction]
fn apply_tx(tx_data: Vec<u8>) {
  // Do anything here
}
}

Inside the validity predicate function, you can read, write or delete any storage value with the functions provided in the tx_prelude from the storage of any account.

To find out about the interface available in a transaction, please check out Rust docs for tx_prelude.

Compile the transaction's code from the template:

make -C wasm/tx_template

This will output a WASM file that can be found in wasm/tx_template/target/wasm32-unknown-unknown/release/tx_template.wasm.

Submit the transaction to the ledger:

anoma client tx --code-path tx_template/tx.wasm

Optionally, you can also attach some data to the transaction from a file with the --data-path argument.

The Intent gossip and Matchmaker

To run an intent gossip node with an RPC server:

anoma node gossip --rpc "127.0.0.1:39111"

To run an intent gossip node with the intent gossip system, a token exchange matchmaker and RPC through which new intents are requested:

anoma node gossip --rpc "127.0.0.1:39111" --matchmaker-path wasm/mm_token_exch.wasm --tx-code-path wasm/tx_from_intent.wasm --ledger-address "127.0.0.1:26657" --source matchmaker --signing-key matchmaker

Mind that matchmaker should be valid key in your wallet.

This pre-built matchmaker implementation is the fungible token exchange mm_token_exch, that is being used together with the pre-built tx_from_intent transaction WASM to submit transaction from matched intents to the ledger.

✋ Example intents

  1. Lets create some accounts:

    anoma wallet key gen --alias alberto --unsafe-dont-encrypt
    anoma client init-account --alias alberto-account --public-key alberto --source alberto
    
    anoma wallet  key gen --alias chisel --unsafe-dont-encrypt
    anoma client init-account --alias christel-account --public-key christel --source christel
    
    anoma wallet key gen --alias bertha --unsafe-dont-encrypt
    anoma client init-account --alias bertha-account --public-key bertha --source bertha
    
    anoma wallet key gen --alias my-matchmaker --unsafe-dont-encrypt
    anoma client init-account --alias my-matchmaker-account --public-key my-matchmaker --source my-matchmaker
    
  2. We then need some tokens:

    anoma client transfer --source faucet --target alberto-account --signer alberto-account --token BTC --amount 1000
    anoma client transfer --source faucet --target bertha-account --signer bertha-account --token ETH --amount 1000
    anoma client transfer --source faucet --target christel-account --signer christel-account --token XAN --amount 1000
    
  3. Lets export some variables:

    export ALBERTO=$(anoma wallet address find --alias alberto-account | cut -c 28- | tr -d '\n')
    export CHRISTEL=$(anoma wallet address find --alias christel-account | cut -c 28- | tr -d '\n')
    export BERTHA=$(anoma wallet address find --alias bertha-account | cut -c 28- | tr -d '\n')
    export XAN=$(anoma wallet address find --alias XAN | cut -c 28- | tr -d '\n')
    export BTC=$(anoma wallet address find --alias BTC | cut -c 28- | tr -d '\n')
    export ETH=$(anoma wallet address find --alias ETH | cut -c 28- | tr -d '\n')
    
  4. Create files with the intents description:

    echo '[{"addr":"'$ALBERTO'","key":"'$ALBERTO'","max_sell":"70","min_buy":"100","rate_min":"2","token_buy":"'$XAN'","token_sell":"'$BTC'","vp_path": "wasm_for_tests/vp_always_true.wasm"}]' > intent.A.data
    
    echo '[{"addr":"'$BERTHA'","key":"'$BERTHA'","max_sell":"300","min_buy":"50","rate_min":"0.7","token_buy":"'$BTC'","token_sell":"'$ETH'"}]' > intent.B.data
    
    echo '[{"addr":"'$CHRISTEL'","key":"'$CHRISTEL'","max_sell":"200","min_buy":"20","rate_min":"0.5","token_buy":"'$ETH'","token_sell":"'$XAN'"}]' > intent.C.data
    
  5. Start the ledger and the matchmaker. Instruct the matchmaker to subscribe to a topic "asset_v1":

    anoma node ledger run
    
    anoma node gossip --rpc "127.0.0.1:39111" --matchmaker-path wasm/mm_token_exch.wasm --tx-code-path wasm/tx_from_intent.wasm --ledger-address "127.0.0.1:26657" --source mm-1 --signing-key mm-1
    
    anoma client subscribe-topic --node "http://127.0.0.1:39111" --topic "asset_v1"
    
  6. Submit the intents (the target gossip node must be running an RPC server):

    anoma client intent --data-path intent.A.data --topic "asset_v1" --signing-key alberto --node "http://127.0.0.1:39111"
    anoma client intent --data-path intent.B.data --topic "asset_v1" --signing-key bertha --node "http://127.0.0.1:39111"
    anoma client intent --data-path intent.C.data --topic "asset_v1" --signing-key christel --node "http://127.0.0.1:39111"
    

    The matchmaker should find a match from these intents and submit a transaction to the ledger that performs the n-party transfers of tokens.

  7. You can check the balances with:

    anoma client balance --owner alberto-account
    anoma client balance --owner bertha-account
    anoma client balance --owner christel-account
    

🤝 Custom matchmaker

A custom matchmaker code can be built from wasm/mm_template.

A matchmaker code must contain the following function, which will be called when a new intent is received:


#![allow(unused)]
fn main() {
use anoma_vm_env::matchmaker_prelude::*;

#[matchmaker]
fn add_intent(last_state: Vec<u8>, intent_id: Vec<u8>, intent_data: Vec<u8>) -> bool {
  // Returns a result of processing the intent
  true
}
}

The matchmaker can keep some state between its runs. The state can be updated from within the matchmaker code with update_state function and received from the last_state argument.

To find out about the interface available in a matchmaker and the library code used in the mm_token_exch implementation, please check out Rust docs for matchmaker_prelude.

Exploration

This section documents the process of exploring the design and implementation space for Anoma. Ideally, the captured information should provide an overview of the explored space and help to guide further decisions.

The content of this section is more free-form. This is largely a cross-over of both the implementation details and the design of implementation-independent specifications.

Design

This section covers the exploration of the possible design directions of the involved components.

Overview

⚠️ This section is WIP.

  • TODO: add high-level interaction diagram(s)

The Rust crates internal dependency graph:

crates Diagram on Excalidraw

The gossip network

The gossip network runs in parallel to the ledger network and is used to propagate off-chain information. The network is based on libp2p , a peer to peer network system that is implemented in different languages, has a large user base and an active development. It allows us to readily implement a network to run our application.

The gossip network is used to propagate messages of two different applications, intents for the intent gossip system, and message for distributed keys generation application.

Flow diagram: High level overview

gossip process

Diagram on Excalidraw

Intent gossip network

The intent gossip network enables counterparty discovery for bartering. The users can express any sort of intents that might be matched and transformed into a transaction that fulfills the intents on the Anoma ledger.

An intent describes the desire of a user, from asset exchange to a green tax percent for selling shoes. These intents are picked up by a matchmaker that composes them into transactions to send to the ledger network. A matchmaker is optionally included in the intent gossip node.

Each node connects to a specified intent gossip network, either a public or a private one. Anyone can create their own network where they decide all aspects of it: which type of intents is propagated, which nodes can participate, the matchmaker logic, etc. It is possible, for example, to run the intent gossip system over bluetooth to have it off-line.

An intent gossip node is a peer in the intent gossip network that has the role of propagating intents to all other connected nodes.

The network uses the gossipsub network behaviour. This system aggregates nodes around topics of interest. Each node subscribes to a set of topics and connects to other nodes that are also subscribed to the same topics. A topic defines a sub-network for a defined interest, e.g. “asset_exchange”. see gossipsub for more information on the network topology.

Each node has an incentive to propagate intents and will obtain a small portion of the fees if the intent is settled. (TODO: update when logic is found) See incentive for more information.

Flow diagram: asset exchange

This example shows three intents matched together by the intent gossip network. These three intents express user desires to exchange assets.

intent gossip and ledger network interaction Diagram on Excalidraw

Flow diagram: life cycle of intent and global process

This diagram shows the process flow for intents, from users expressing their desire to the ledger executing the validity predicate to check the crafted transaction.

intent life cycle Diagram on Excalidraw

Intents

An intent is a way of expressing a user's desire. It is defined as arbitrary data and an optional address for a schema. The data is as arbitrary as possible to allow the users to express any sort of intent. It could range from defining a selling order for a specific token to offering piano lessons or even proposing a green tax for shoes’ manufacturers.

An intent is written using an encoding, or data schema. The encoding exists either on-chain or off-chain. It must be known by users that want to express similar intents. It also must be understood by some matchmaker. Otherwise, it possibly won’t be matched. The user can define its own schema and inform either off-chain or on-chain. Having it on-chain allows it to easily share it with other participants. Please refer to data schema for more information about the usage of on-chain schema.


There is only a single intent type that is composed of arbitrary data and a possible schema definition.


#![allow(unused)]
fn main() {
struct Intent {
    schema: Option<Key>,
    data: Vec<u8>,
    timestamp: Timestamp
}
}

Topic

A topic is string and an encoding that describes this sub-network. In a topic all intents use the exact same encoding. That encoding is known by matchmakers so it can decode them to find matches. Whenever a node subscribes to a new topic it informs all connected nodes and each of them propagate it. With this it’s easy to create new topics in the intent gossip network and inform others.

Other nodes can choose to subscribe to a new topic with the help of a filter. This filter is defined as a combination of a whitelist, a regex expression, and a maximum limit.

Incentive

Tracking Issue


TODO

  • describe incentive function
  • describe logic to ensure matchmaker can't cheat intent gossip service

Matchmaker

The matchmaker is a specific actor in the intent gossip network that tries to match intents together. When intents are matched together, the matchmaker crafts a transaction from them and sends it to the ledger network.

A matchmaker is an intent gossip node started with additional parameters: a ledger address and a list of sub-matchmakers. A sub-matchmaker is defined with a topics list, a main program path, a filter program path, and a transaction code.

The main and filter programs are wasm compiled code. Each has a defined entrypoint and their own set of environment functions that they can call.

When the matchmaker receives a new intent from the network it calls the corresponding sub-matchmaker, the one that has the intent’s topic in their topics list. A sub-matchmaker first checks if the intent is accepted by the filter, before adding it to that sub-matchmaker database. Then the main program is called with the intent and current state.

Sub-matchmaker topics' list

A sub-matchmaker is defined to work with only a subset of encoding. Each intent propagated to the corresponding topic will be process by this sub-matchmaker.

Having a topics list instead of a unique topic allows a matchmaker to match intents from different encodings. For example, when an updated version of an encoding is out, the matchmaker could match intents from both versions if they don’t diverge too much.

Sub-matchmaker database and state (name TBD)

Each sub-matchmaker has a database and an arbitrary state.

The database contains intents received by the node from the topics list that passed the filter.

The state is arbitrary data that is managed by the main program. That state is given to all calls in the main program.

The database is persistent but the state is not. When a node is started the state is recovered by giving all intents from the database to the main program. The invariant that the current state is equal to the state if the node is restarted is not enforced and is the responsibility of the main program.

Filter program

The filter is an optional wasm program given in parameters. This filter is used to check each intent received by that sub-matchmaker. If it's not defined, intents are directly passed to the main program.

The entrypoint filter_intent takes an intent and returns a boolean. The filter has the ability to query the state of the ledger for any given key.

Main program

The main program is a mandatory wasm program given in parameters. The main program must match together intents.

The main program entrypoint match_intent takes the current state, a new intent data and its id. The main program also has the ability to query the state of the ledger. It also has functions remove and get to interact with the matchmaker mempool. When a main matchmaker program finds a match it sends a transaction to the ledger composed of the code template given in the matchmaker parameter and the data given to this function. Finally the matchmaker must update its state so the next run will have up to date values.

The main program is called on two specific occasion; when intent gossip node is started, on all intent from database and whenever a new intent is received from the p2p network and the RPC endpoint, if enabled.

Transaction

The transaction code given in parameters is used when the main program matches a group of intents. The main program returns arbitrary data that is attached to the transaction which is then injected into a ledger node.

Flow diagram: Matchmaker process

matchmaker process

excalidraw link

Fungible token encoding and template

The Heliax team implemented an intent encoding, a filter program template, and a matchmaker program template that can be used to exchange fungible tokens between any number of participants.

Intent encoding

The intent encoding allows the expression of a desire to participate in an asset exchange. The encoding is defined as follows :

message FungibleToken {
  string address = 1;
  string token_sell = 2;
  int64 max_sell = 3;
  int64 rate_min = 4;
  string token_buy = 5;
  int64 min_buy = 6;
  google.protobuf.Timestamp expire = 7;
}

Matchmaker program

The filter program attempts to decode the intent and if successful, checks that it's not yet expired and that the account address has enough funds for the intended token to be sold.

The main program can match intents for exchanging assets. It does that by creating a graph from all intents. When a cycle is found then it removes all intents from that cycle of the mempool and crafts a transaction based on all the removed intents.

matchmaker excalidraw link

Distributed key generation gossip

⚠️ This section is WIP.

The ledger

The ledger depends on Tendermint node. Running the Anoma node will also initialize and run Tendermint node. Anoma communicates with Tendermint via the ABCI.

Tendermint ABCI

We are using the Tendermint state-machine replication engine via ABCI. It provides many useful things, such as a BFT consensus protocol, P2P layer with peer exchange, block sync and mempool layer.

Useful resources:

Rust ABCI implementations:

ABCI Integration

The ledger wraps the Tendermint node inside the Anoma node. The Tendermint node communicates with the Anoma shell via four layers as illustrated below.

flowchart LR
    C[Client] --- R
    subgraph Anoma Node
    S((Anoma Shell))
    subgraph Tendermint ABCI
    R[RPC] === T{Tendermint}
    T --- TC[Consensus]
    T --- TM[Mempool]
    T --- TQ[Query]
    T --- TS[Snapshot]
    end
    TC --- S
    TM --- S
    TQ --- S
    TS --- S
    end

The consensus connection allows the shell to:

  • initialize genesis on start-up
  • begin a block
  • apply a transaction(s) in a block
  • end a block
  • commit a block

The mempool connection asks the shell to validate transactions before they get stored in the mempool and broadcasted to peers. The mempool will signify that the transaction is either new, when it has not been validated before, or to be re-checked when it has been validated at some previous level.

The query connection is used for:

  • the Tendermint node asks the last known state from the shell to determine if it needs to replay any blocks
  • relay client queries for some state at a given path to the shell

The snapshot connection is used to serve state sync snapshots for other nodes and/or restore state sync snapshots to a local node being bootstrapped.

Parameters

The parameters are used to dynamically control certain variables in the protocol. They are implemented as an internal address with a native VP. The current values are written into and read from the block storage in the parameters account's sub-space.

Initial parameters for a chain are set in the genesis configuration. On chain, these can be changed by 2/3 of voting power (specifics are TBA).

Epoch duration

The parameters for epoch duration are:

  • Minimum number of blocks in an epoch
  • Minimum duration of an epoch

Epochs

An epoch is a range of blocks whose length is determined by the epoch duration protocol parameter: minimum epoch duration and minimum number of blocks in an epoch. They are identified by consecutive natural numbers starting at 0.

We store the current epoch in global storage and the epoch of each block in the block storage. We also store the minimum height and minimum time of a first block in the next epoch in global storage, so that changes to the epoch duration protocol parameter don't affect the current epoch, but rather apply from the following epoch. Note that protocol parameters changes may themselves be delayed.

The first epoch (ID 0) starts on the genesis block. The next epoch minimum start time is set to the genesis time configured for the chain + minimum duration and the next epoch minimum height is set to the height of the genesis block (typically 1) + minimum number of blocks.

On each block BeginBlock Tendermint call, we check if the current epoch is finished, in which case we move on to the next epoch. An epoch is finished when both the minimum number of blocks and minimum duration of an epoch have been created from the first block of a current epoch. When a new epoch starts, the next epoch minimum height is set to the block's height + minimum number of blocks and minimum start time time is set to block's time from the block header + minimum duration.

Predecessor blocks epochs

We store the epoch ranges of predecessor blocks. This is used for example for to look-up the epoch from an evidence of validators that acted maliciously (which includes block height and block time) for PoS system. For the PoS system, in block at height h, we only need to know values from Tendermint max(h - consensus_params.evidence.max_age_num_blocks, 0), which is set to 100000 by default.

The predecessor epochs are stored in the block storage. We update this structure on every new epoch and trim any epochs that ended more than max_age_num_blocks ago.

Accounts

Tracking Issue


There's only a single account type. Each account is associated with:

Addresses

There are two main types of address: transparent and shielded.

The transparent addresses are the addresses of accounts associated with dynamic storage sub-spaces, where the address of the account is the prefix key segment of its sub-space.

The shielded addresses are used for private transactions and they are not directly associated with storage sub-spaces.

Transparent addresses

Furthermore, there are three types of transparent addresses:

  • "implicit" addresses which are derived from public keys
  • "established" addresses which are generated from the current address nonce and hence must be created via a request in the ledger
  • "internal" addresses are used for special modules integrated into the ledger such as PoS and IBC.

The addresses are stored on-chain encoded with bech32m (not yet adopted in Zcash), which is an improved version of bech32.

The human-readable prefix (as specified for bech32) in the address encoding is:

  • "a" for Anoma live network
  • "atest" for test network

Implicit transparent addresses

As implied by their name, accounts for implicit addresses exist as a possibility and not as a matter of fact. These addresses allow users to interact with public keys which may or may not have a registered on-chain account, e.g. allowing to send some fungible token to an address derived from a public key. An implicit address is derived from a hash of a public key, which also helps to protect keys for which the public key has not been revealed publicly.

Established transparent addresses

Established addresses are created by a ledger transaction, which can create any number of new account addresses. The users are not in control of choosing the address as it's derived from the current address nonce, which is changed after every newly established address.

Internal transparent addresses

There will be a static set of internal addresses that integrate certain functionality into the ledger via a dedicated module, such as the proof-of-stake module and the IBC module. The internal accounts use native validity predicates to validate transactions that interact with their module. A native module will use the dynamic storage sub-space to store all the data relevant to their functionality (e.g. PoS parameters, bond pool, IBC state and proofs).

Shielded addresses

Similar to Zcash Sapling protocol payment addresses and keys (section 3.1), users can generate spending keys for private payments. A shielded payment address, incoming viewing key and full viewing key are derived from a spending key. In a private payment, a shielded payment address is hashed with a diversifier into a diversified transmission key. When a different diversifier function is chosen for different transactions, it prevents the transmission key from being matched across the transactions.

The encoding of the shielded addresses, spending and viewing keys is not yet decided, but for consistency we'll probably use a the same schema with different prefixes for anything that can use an identifier.

  • TODO consider using a schema similar to the unified addresses proposed in Zcash, that are designed to unify the payment addresses across different versions by encoding a typecode and the length of the payment address together with it. This may be especially useful for the protocol upgrade system and fractal scaling system.

Dynamic storage sub-space

Each account can have an associated dynamic account state in the storage. This state may be comprised of keys of the built-in supported types and values of arbitrary user bytes.

The dynamic storage sub-space could be a unix filesystem-like tree under the account's address key-space with read, write, delete, has_key, iter_prefix (and maybe a few other convenience functions for hash-maps, hash-sets, optional values, etc.) functions parameterized with the the account's address.

In addition, the storage sub-space would provide:

  • a public type/trait for storage keys and key segments:
    • this should allow to turn types to storage key segments, key segments back to types
    • combine key segments into keys
    • can be extended with custom types in the code in a transaction
  • a public type/trait for storage values:
    • values need to implement encoding traits, e.g. BorshSerialize, BorshDeserialize
      • this allows composition of types as specified for Borsh
      • the Merkle tree hashing function should hash values from the encoded bytes of this trait (the encoded value may be cached, because we update the Merkle tree in-memory before we commit the finalized block to the DB)
  • functions to get the size of a key and an encoded value (for storage fees)
  • the updates to account storage should be immediately visible to the transaction that performed the updates
    • validity predicate modifications have to be handled a little differently - the old validity predicate should be run to check that the new validity predicate (and other state changes included in the transaction) is valid

Initializing a new account

A new account can be initialized on-chain with a transaction:

  • anything be written into its storage (initial parameter)
  • a validity predicate has to be provided (we can have a default out-of-band)
  • at minimum, accounts need to be enumerated on chain, this could be done with an address or a counter

A newly created account should be validated by all the VPs triggered by the transaction, i.e. it should be included in the set of changed keys passed to each VP. If the VPs are not interested in the newly created account, they can choose to ignore it.

Validity predicates

Tracking Issue


Each account is associated with exactly one validity predicate (VP).

Conceptually, a VP is a function from the transaction's data and the storage state prior and posterior to a transaction execution returning a boolean value. A transaction may modify any data in the accounts' dynamic storage sub-space. Upon transaction execution, the VPs associated with the accounts whose storage has been modified are invoked to verify the transaction. If any of them reject the transaction, all of its storage modifications are discarded.

There are some native VPs for internal transparent addresses that are built into the ledger. All the other VPs are implemented as WASM programs. One can build a custom VP using the VP template or use one of the pre-defined VPs.

The VPs must implement the following interface that will be invoked by the protocol:


#![allow(unused)]
fn main() {
fn validate_tx(
    // Data of the transaction that triggered this VP call
    tx_data: Vec<u8>,
    // Address of this VP
    addr: Address,
    // Storage keys that have been modified by the transation, relevant to this VP
    keys_changed: HashSet<storage::Key>,
    // Set of all the addresses whose VP was triggered by the transaction
    verifiers: HashSet<Address>,
) -> bool;
}

The host functions available to call from inside the VP code can be found in docs generated from code.

Native VPs

The native VPs follow the same interface as WASM VPs and rules for how they are triggered by a transaction. They can also call the same host functions as those provided in WASM VPs environment and must also account any computation for gas usage.

PoS slash pool VP

The Proof-of-Stake slash pool is a simple account with a native VP which can receive slashed tokens, but no token can ever be withdrawn from it by anyone at this point.

Fungible token VP

The fungible token VP allows to associate accounts balances of a specific token under its account.

For illustration, users Albert and Bertha might hold some amount of token with the address XAN. Their balances would be stored in the XAN's storage sub-space under the storage keys @XAN/balance/@Albert and @XAN/balance/@Bertha, respectively. When Albert or Bertha attempt to transact with their XAN tokens, its validity predicate would be triggered to check:

  • the total supply of XAN token is preserved (i.e. inputs = outputs)
  • the senders (users whose balance has been deducted) are checked that their validity predicate has also been triggered

Note that the fungible token VP doesn't need to know whether any of involved users accepted or rejected the transaction, because if any of the involved users rejects it, the whole transaction will be rejected.

User VP

The user VP currently provides a signature verification against a public key for sending tokens as prescribed by the fungible token VP. In this VP, a transfer of tokens doesn't have to be authorized by the receiving party.

It also allows arbitrary storage modifications to the user's sub-space to be performed by a transaction that has been signed by the secret key corresponding to the user's public key stored on-chain. This functionality also allows one to update their own validity predicate.

Transactions

Tracking Issue


There is only a single general transaction (tx) type:


#![allow(unused)]
fn main() {
struct Transaction {
    // A wasm module with a required entrypoint
    code: Vec<u8>
    // Optional arbitrary data
    data: Option<Vec<u8>>,
    // A timestamp of when the transaction was created
    timestamp: Timestamp,
    gas_limit: TODO,
}
}

The tx allows to include arbitrary data, e.g zero-knowledge proofs and/or arbitrary nonce bytes to obfuscate the tx's minimum encoded size that may be used to derive some information about the tx.

TODO once we have DKG, we will probably want to have some kind of a wrapper transaction with submission fees, payer and signature

Tx life cycle

flowchart TD
    subgraph Node
    I[Initialize chain] --> Begin
    Begin[Begin block] --> Poll
    Poll[Poll mempool queue] --> Apply
    Apply[Apply txs] --> End
    End[End block] --> Commit[Commit block]
    Commit --> Begin
    Commit --> Flush
      subgraph Mempool
      Validate --> V{is valid?}
      V -->|Yes| Add[Add to local queue]
      V -->|No| Fail[Drop tx]
      Flush -->|Re-validate txs not included in this block| V
      end
    end
    subgraph Client
    Submit[Submit tx] --> Validate
    end

New txs are injected by the client via mempool. Before including a tx in a local mempool queue, some cheap validation may be performed. Once a tx is included in a mempool queue, it will be gossiped with the peers and may be included in a block by the block proposer. Any txs that are left in the queue after flush will be subject to re-validation before being included again.

The order of applying transactions within a block is fixed by the block proposer in the front-running prevention protocol.

TODO we might want to randomize the tx order after DKG protocol is completed

Block application

Within a block, each tx is applied sequentially in three steps:

flowchart TD
    B[Begin block] --> N{Has next tx and within block gas limit?}
    N --> |Yes|E
    N -----> |No|EB[End block]
    E[Exec tx code] -->|"∀ accounts with modified storage"| VP[Run validity predicates in parallel]
    VP --> A{all accept}
    A --> |No|R[Reject tx]
    A --> |Yes|C[Commit tx and state changes]
    R --> N
    C --> N

Tx execution

The code is allowed to read and write anything from accounts' sub-spaces and to initialize new accounts. Other data that is not in an account's subspace is read-only, e.g. chain and block metadata, account addresses and potentially keys.

In addition to the verifiers specified in a transaction, each account whose sub-space has been modified by the tx triggers its VP.

For internal addresses, we invoke their module's native VP interface directly. For other addresses, we look-up validity predicates WASM to be executed from storage.

The VPs are then given the prior and posterior state from the account's sub-space together with the tx to decide if it accepts the tx's state modifications.

Within a single tx the execution of the validity predicates will be parallelized and thus the fee for VPs execution would their maximum value (plus some portion of the fees for each of the other parallelized VPs - nothing should be "free"). Once any of the VPs rejects the modifications, execution is aborted, the transaction is rejected and state changes discarded. If all the VPs accept the modifications, the transaction is successful and modifications are committed to storage as the input of the next tx.

The transaction's API should make it possible to transfer tokens to a hash of a public key that is not revealed. This could be done by having a "deposit" account from which the key's owner can claim the deposited funds.

Should some type of token prefer not to allow to receive tokens without recipient's approval, a token account can implement logic to decline the received tokens.

WASM VM

A wasm virtual machine will be used for validity predicates and transactions code.

The VM should provide:

Resources

Wasm environment

The wasm environment will most likely be libraries that provide APIs for the wasm modules.

Common environment

The common environment of VPs and transactions APIs:

  • math & crypto
  • logging
  • panics/aborts
  • gas metering
  • storage read-only API
  • context API (chain metadata such as block height)

The accounts sub-space storage is described under accounts' dynamic storage sub-space.

VPs environment

Because VPs are stateless, everything that is exposed in the VPs environment should be read-only:

Transactions environment

Some exceptions as to what can be written are given under transaction execution.

Wasm memory

The wasm memory allows to share data bi-directionally between the host (Rust shell) and the guest (wasm) through a wasm linear memory instance.

Because wasm currently only supports basic types, we need to choose how to represent more sophisticated data in memory.

The options on how the data can be passed through the memory are:

The choice should allow for easy usage in wasm for users (e.g. in Rust a bindgen macro on data structures, similar to wasm_bindgen used for JS <-> wasm).

Related wasmer issue.

We're currently using borsh for storage serialization, which is also a good option for wasm memory.

  • it's easy for users (can be derived)
  • because borsh encoding is safe and consistent, the encoded bytes can also be used for Merkle tree hashing
  • good performance, although it's not clear at this point if that may be negligible anyway

The data

The data being passed between the host and the guest in the order of the execution:

  • For transactions:
    • host-to-guest: pass tx.data to tx.code call
    • guest-to-host: parameters of environment functions calls, including storage modifications (pending on storage API)
    • host-to-guest: return results for host calls
  • For validity predicates:
    • host-to-guest: pass tx.data, prior and posterior account storage sub-space state and/or storage modifications (i.e. a write log) for the account
    • guest-to-host: parameters of environment function calls
    • host-to-guest: return results for host calls
    • guest-to-host: the VP result (bool) can be passed directly from the call

Storage write log

The storage write log gathers any storage updates (write/deletes) performed by transactions. For each transaction, the write log changes must be accepted by all the validity predicates that were triggered by these changes.

A validity predicate can read its prior state directly from storage as it is not changed by the transaction directly. For the posterior state, we first try to look-up the keys in the write log to try to find a new value if the key has been modified or deleted. If the key is not present in the write log, it means that the value has not changed and we can read it from storage.

The write log of each transaction included in a block and accepted by VPs is accumulated into the block write log. Once the block is committed, we apply the storage changes from the block write log to the persistent storage.

write log Diagram on Excalidraw

Gas metering

The two main options for implementing gas metering within wasm using wasmer are:

Both of these allow us to assign a gas cost for each wasm operation.

wasmer gas middleware is more recent, so probably more risky. It injects the gas metering code into the wasm code, which is more efficient than host calls to a gas meter.

pwasm-utils divides the wasm code into metered blocks. It performs host call with the gas cost of each block before it is executed. The gas metering injection is linear to the code size.

The pwasm-utils seems like a safer option to begin with (and we'll probably need to use it for stack height metering too). We can look into switching to wasmer middleware at later point.

Stack height metering

For safety, we need to limit the stack height in wasm code. Similarly to gas metering, we can also use wasmer middleware or pwasm-utils.

We have to use pwasm-utils, because wasmer's stack limiter is currently non-deterministic (platform specific). This is to be fixed in this PR: https://github.com/wasmerio/wasmer/pull/1037.

Front-running prevention

Tracking Issue


This page should describe how DKG can be integrated for front-running prevention.

Fractal scaling

Tracking Issue


Upgrade system

Tracking Issue


Storage

We can make use of the Tendermint's finality property to split the storage into immutable and mutable parts, where only the data at the current level is mutable. It should be possible to have the mutable state in-memory only and write to DB only once a block is finalized, which combined with batch writes would most likely be quite efficient (this can also be done asynchronously).

graph LR
  subgraph "in-memory"
    LN[level n]
  end
  subgraph "DB (each level is immutable once written)"
    LN .-> LNL[level n - 1]
    LNL ===== L0[level 0]
  end

In-memory (mutable state)

The current state is stored in a Sparse Merkle tree. The layout of data in memory should be flexible to allow to optimize throughput. For example, the values of key/value pairs may better stored in a sequence outside of the tree structure. Furthermore, it maybe be better to have the data sorted in memory. This may be possible by decoupling the merkle tree structure from the data and the key/value pairs, as illustrated below.

graph TD
  subgraph storage
    subgraph sparse merkle tree
      B[branches as paths segments in hashes of keys] .-> L[leaves as a hashes of values]
    end
    subgraph columns
      KV[dictionaries of key/value pairs]
    end
  end

It may be advantageous if the data columns keys are not hashed to preserve ordering.

DB (immutable state)

The immutable state doesn't have the same requirements as the mutable. This means that a different data structures or memory layout may perform better (subject to benchmarks). The state trees in the immutable blocks should take advantage of its properties for optimization. For example, it can save storage space by sharing common data and/or delta compression.

It's very likely that different settings for immutable storage will be provided in future, similar to e.g. Tezos history modes.

Benchmarks

We'd like to have easily reproducible benchmarks for the whole database integration that should be filled over time with pre-generated realistic data. This should enable us to tune and compare different hashing functions, backends, data structures, memory layouts, etc.

Criteria

  • in-memory
    • writes (insert, update, delete)
      • possibly also concurrent writes, pending on the approach taken for concurrent transaction execution
    • reads
    • proof generation (inclusion, non-inclusion)
  • DB (lower priority)
    • writes in batched mode
    • reads
    • proof generation (inclusion, non-inclusion)

DB backends

The considered options for a DB backend are given in Libraries & Tools / Database page.

RocksDB

A committed block is not immediately persisted on RocksDB. When the block is committed, a set of key-value pairs which compose the block is written to the memtable on RocksDB. For the efficient sequential write, a flush is executed to persist the data on the memtable to the disk as a file when the size of the memtable is getting big (the threshold is one of the tuning parameters).

We can disable write-ahead log(WAL) which protects these data on the memtable from a crash by persisting the write logs to the disk. Disabling WAL helps reduce the write amplification. That's because WAL isn't required for Anoma because other nodes have the block. The blocks which have not been persisted to the disk by flush can be recovered even if an Anoma node crashes.

Implementation

storage module

This is the main interface for interacting with storage in Anoma.

This module and its sub-modules should implement the in-memory storage (and/or a cache layer) with Merkle tree (however, the interface should be agnostic to the choice of vector commitment scheme or whether or not there even is one, we may want non-Merklised storage) and the persistent DB.

The in-memory storage holds chain's metadata and current block's storage.

Its public API should allow/provide:

  • get the Merkle root and Merkle tree proofs
  • read-only storage API for ledger's metadata to be accessible for transactions' code, VPs and the RPC
    • with public types of all the stored metadata
  • unless specified otherwise, read the state from the current block

An API made visible only to the shell module (e.g. pub ( in SimplePath ) - https://doc.rust-lang.org/reference/visibility-and-privacy.html) should allow the shell to:

  • load state from DB for latest persisted block or initialize a new storage if none found
  • begin a new block
  • within a block:
    • transaction can modify account sub-space
      • the function that modify storage (e.g. write and delete) have to guarantee to also update the Merkle tree
    • store each applied transaction and its result
  • end the current block
  • commit the current block (persist to storage)

storage/db module

The persistent DB implementation (e.g. RocksDB).

DB keys

The DB keys are composed of key segments. A key segment can be an Address which starts with # (there can be multiple addresses involved in a key) or any user defined non-empty utf-8 string (maybe limited to only alphanumerical characters). Also, / and ? are reserved. / is used as a separator for segments. ? is reserved for a validity predicate and the key segment ? can be specified only by the specific API.

In the DB storage, the keys would be prefixed by the block height and the space type. This would be hidden from the wasm environment, which only operates at the current block height. For example, when the block height is 123 and the key specified by the storage is #my_address_hash/balance/token, the actual key for the persistent DB implementation would be 123/subspace/#my_address_hash/balance/token.

This could roughly be implemented as:

struct Key {
    segments: Vec<DbKeySeg>
}

impl Key {
    fn parse(string: String) -> Result<Self, Error> {..}
    fn push(&self, other: &KeySeg) -> Self {..}
    fn join(&self, other: &Key) -> Self {..}
    fn into_string(&self) -> String;
    // find addresses included in the key, used to find which validity-predicates should be triggered by a key space change
    fn find_addresses(&self) -> Vec<Address> {..}
}

// Provide a trait so that we can define new pre-defined key segment types inside wasm environment and also ad-hoc key segments defined by wasm users
trait KeySeg {
    fn parse(string: String) -> Result<Self, Error>;
    fn to_string(&self) -> String;
    fn to_db_key(&self) -> DbKeySeg;
}

enum DbKeySeg {
    AddressSeg(Address),
    StringSeg(String),
}

impl KeySeg for DbKeySeg {..}
impl KeySeg for BlockHeight {..}

Then the storage API functions (read/write/delete) should only accept the keys with this Key type.

Data schema

At high level, all the data in the accounts' dynamic sub-spaces is just keys associated with arbitrary bytes and intents are just wrapper around arbitrary data. To help the processes that read and write this data (transactions, validity predicates, matchmaker) interpret it and implement interesting functionality on top it, the ledger could provide a way to describe the schema of the data.

For storage data encoding, we're currently using the borsh library, which provides a way to derive schema for data that can describe its structure in a very generic way that can easily be consumed in different data-exchange formats such as JSON. In Rust code, the data can be composed with Rust native ADTs (struct and enum) and basic collection structures (fixed and dynamic sized array, hash map, hash set). Borsh already has a decent coverage of different implementations in e.g. JS and TypeScript, JVM based languages and Go, which we'll hopefully be able to support in wasm in near future too.

Note that the borsh data schema would not be forced upon the users as they can still build and use custom data with arbitrary encoding.

A naive implementation could add optional schema field to each stored key. To reduce redundancy, there could be some "built-in" schemas and/or specific storage space for commonly used data schema definitions. Storage fees apply, but perhaps they can be split between all the users, so some commonly used data schema may be almost free.

A single address in the ledger is define with all schema. A specific schema can be looked up with a key in its subspace. The schema variable is not yet implemented and the definition might change to something more appropiate.

Schema derived library code

account example

Let's start with an example, in which some users want to deploy a multi-signature account to some shared asset. They create a transaction, which would initialize a new account with an address shared-savings and write into its storage sub-space the initial funds for the account and data under the key "multisig" with the following definition:


#![allow(unused)]
fn main() {
#[derive(Schema)]
struct MultiSig {
    threshold: u64,
    counter: u64,
    keys: Vec<PublicKey>,
}
}

When the transaction is applied, the data is stored together with a reference to the derived data schema, e.g.:

{
  "MultiSig": {
    "struct": {
      "named_fields": {
        "threshold": "u64",
        "counter": "u64",
        "keys": {
          "sequence": "PublicKey"
        }
      }
    }
  }
}

Now any transaction that wants to interact with this account can look-up and use its data schema. We can also use this information to display values read from storage from e.g. RPC or indexer.

What's more, when the data has schema attached on-chain, with borsh we have bijective mapping between the data definitions and their schemas. We can use this nice property to generate code for data definitions back from the schema in any language supported by borsh and that we'll able to support in wasm.

We can take this a step further and even generate some code for data access on top of our wasm environment functions to lift the burden of encoding/decoding data from storage. For our example, from the key "multisig", in Rust we can generate this code:


#![allow(unused)]
fn main() {
fn read_multisig() -> MultiSig;
fn write_multisig(MultiSig);
fn with_multisig(FnMut(MultiSig) -> MultiSig);
}

Which can be imported like regular library code in a transaction and arbitrarily extended by the users. Similarly, the schema could be used to derive some code for validity predicates and intents.

We can generate the code on demand (e.g. we could allow to query a node to generate library code for some given accounts for a given language), but we could also provide some helpers for e.g. foundation's or validator's node to optionally automatically publish generated code via git for all the accounts in the current state. In Rust, using this library could look like this:


#![allow(unused)]
fn main() {
// load the account(s) code where the identifier is the account's address.
use anoma_accounts::SharedSavings;

fn transaction(...) {
  let multisig = SharedSavings::read_multisig();
  ...
}
}

PoS integration

The PoS system is integrated into Anoma ledger at 3 different layers:

  • base ledger that performs genesis initialization, validator set updates on new epoch and applies slashes when they are received from ABCI
  • an account with an internal address and a native VP that validates any changes applied by transactions to the PoS account state
  • transaction WASMs to perform various PoS actions, also available as a library code for custom made transactions

The votes_per_token PoS system parameter must be chosen to satisfy the Tendermint requirement of MaxTotalVotingPower = MaxInt64 / 8.

All the data relevant to the PoS system are stored under the PoS account's storage sub-space, with the following key schema (the PoS address prefix is omitted for clarity):

  • params (required): the system parameters

  • for any validator, all the following fields are required:

    • validator/{validator_address}/consensus_key
    • validator/{validator_address}/state
    • validator/{validator_address}/total_deltas
    • validator/{validator_address}/voting_power
  • slash/{validator_address} (optional): a list of slashes, where each record contains epoch and slash rate

  • bond/{bond_source}/{bond_validator} (optional)

  • unbond/{unbond_source}/{unbond_validator} (optional)

  • validator_set (required)

  • total_voting_power (required)

  • standard validator metadata (these are regular storage values, not epoched data):

    • validator/{validator_address}/staking_reward_address (required): an address that should receive staking rewards
    • validator/{validator_address}/address_raw_hash (required): raw hash of validator's address associated with the address is used for look-up of validator address from a raw hash
    • TBA (e.g. alias, website, description, delegation commission rate, etc.)

Only XAN tokens can be staked in bonds. The tokens being staked (bonds and unbonds amounts) are kept in the PoS account under {xan_address}/balance/{pos_address} until they are withdrawn.

Initialization

The PoS system is initialized via the shell on chain initialization. The genesis validator set is given in the genesis configuration. On genesis initialization, all the epoched data is set to be immediately active for the current (the very first) epoch.

Staking rewards and transaction fees

Staking rewards for validators are rewarded in Tendermint's method BeginBlock in the base ledger. A validator must specify a validator/{validator_address}/staking_reward_address for its rewards to be credited to this address.

To a validator who proposed a block (block.header.proposer_address), the system rewards tokens based on the block_proposer_reward PoS parameter and each validator that voted on a block (block.last_commit_info.validator who signed_last_block) receives block_vote_reward.

All the fees that are charged in a transaction execution (DKG transaction wrapper fee and transactions applied in a block) are transferred into a fee pool, which is another special account controlled by the PoS module. Note that the fee pool account may contain tokens other than the staking token XAN.

Transactions

The transactions are assumed to be applied in epoch n. Any transaction that modifies epoched data updates the structure as described in epoched data storage.

For slashing tokens, we implement a PoS slash pool account. Slashed tokens should be credited to this account and, for now, no tokens can be be debited by anyone.

Validator transactions

The validator transactions are assumed to be applied with an account address validator_address.

  • become_validator(consensus_key, staking_reward_address):
    • creates a record in validator/{validator_address}/consensus_key in epoch n + pipeline_length
    • creates a record in validator/{validator_address}/staking_reward_address
    • sets validator/{validator_address}/state for to pending in the current epoch and candidate in epoch n + pipeline_length
  • deactivate:
    • sets validator/{validator_address}/state for to inactive in epoch n + pipeline_length
  • reactivate:
    • sets validator/{validator_address}/state for to pending in the current epoch and candidate in epoch n + pipeline_length
  • self_bond(amount):
    • let bond = read(bond/{validator_address}/{validator_address}/delta)
    • if bond exist, update it with the new bond amount in epoch n + pipeline_length
    • else, create a new record with bond amount in epoch n + pipeline_length
    • debit the token amount from the validator_address and credit it to the PoS account
    • add the amount to validator/{validator_address}/total_deltas in epoch n + pipeline_length
    • update the validator/{validator_address}/voting_power in epoch n + pipeline_length
    • update the total_voting_power in epoch n + pipeline_length
    • update validator_set in epoch n + pipeline_length
  • unbond(amount):
    • let bond = read(bond/{validator_address}/{validator_address}/delta)
    • if bond doesn't exist, panic
    • let pre_unbond = read(unbond/{validator_address}/{validator_address}/delta)
    • if total(bond) - total(pre_unbond) < amount, panic
    • decrement the bond deltas starting from the rightmost value (a bond in a future-most epoch) until whole amount is decremented
    • for each decremented bond value write a new unbond with the key set to the epoch of the source value
    • decrement the amount from validator/{validator_address}/total_deltas in epoch n + unbonding_length
    • update the validator/{validator_address}/voting_power in epoch n + unbonding_length
    • update the total_voting_power in epoch n + unbonding_length
    • update validator_set in epoch n + unbonding_length
  • withdraw_unbonds:
    • let unbond = read(unbond/{validator_address}/{validator_address}/delta)
    • if unbond doesn't exist, panic
    • if no unbond value is found for epochs <= n, panic
    • for each ((bond_start, bond_end), amount) in unbond where unbond.epoch <= n:
      • let amount_after_slash = amount
      • for each slash in read(slash/{validator_address}):
        • if bond_start <= slash.epoch && slash.epoch <= bond_end), amount_after_slash *= (10_000 - slash.rate) / 10_000
      • credit the amount_after_slash to the validator_address and debit the whole amount (before slash, if any) from the PoS account
      • burn the slashed tokens (amount - amount_after_slash), if not zero
  • change_consensus_key:
    • creates a record in validator/{validator_address}/consensus_key in epoch n + pipeline_length

For self_bond, unbond, withdraw_unbonds, become_validator and change_consensus_key the transaction must be signed with the validator's public key. Additionally, for become_validator and change_consensus_key we must attach a signature with the validator's consensus key to verify its ownership. Note that for self_bond, signature verification is also performed because there are tokens debited from the validator's account.

Delegator transactions

The delegator transactions are assumed to be applied with an account address delegator_address.

  • delegate(validator_address, amount):
    • let bond = read(bond/{delegator_address}/{validator_address}/delta)
    • if bond exist, update it with the new bond amount in epoch n + pipeline_length
    • else, create a new record with bond amount in epoch n + pipeline_length
    • debit the token amount from the delegator_address and credit it to the PoS account
    • add the amount to validator/{validator_address}/total_deltas in epoch n + pipeline_length
    • update the validator/{validator_address}/voting_power in epoch n + pipeline_length
    • update the total_voting_power in epoch n + pipeline_length
    • update validator_set in epoch n + pipeline_length
  • undelegate(validator_address, amount):
    • let bond = read(bond/{delegator_address}/{validator_address}/delta)
    • if bond doesn't exist, panic
    • let pre_unbond = read(unbond/{delegator_address}/{validator_address}/delta)
    • if total(bond) - total(pre_unbond) < amount, panic
    • decrement the bond deltas starting from the rightmost value (a bond in a future-most epoch) until whole amount is decremented
    • for each decremented bond value write a new unbond with the key set to the epoch of the source value
    • decrement the amount from validator/{validator_address}/total_deltas in epoch n + unbonding_length
    • update the validator/{validator_address}/voting_power in epoch n + unbonding_length
    • update the total_voting_power in epoch n + unbonding_length
    • update validator_set in epoch n + unbonding_length
  • redelegate(src_validator_address, dest_validator_address, amount):
    • undelegate(src_validator_address, amount)
    • delegate(dest_validator_address, amount) but set in epoch n + unbonding_length instead of n + pipeline_length
  • withdraw_unbonds:
    • for each validator_address in iter_prefix(unbond/{delegator_address}):
      • let unbond = read(unbond/{validator_address}/{validator_address}/delta)
      • if no unbond value is found for epochs <= n, continue to the next validator_address
      • for each ((bond_start, bond_end), amount) in epochs <= n:
        • let amount_after_slash = amount
        • for each slash in read(slash/{validator_address}):
          • if bond_start <= slash.epoch && slash.epoch <= bond_end), amount_after_slash *= (10_000 - slash.rate) / 10_000
        • credit the amount_after_slash to the delegator_address and debit the whole amount (before slash, if any) from the PoS account
        • burn the slashed tokens (amount - amount_after_slash), if not zero

For delegate, undelegate, redelegate and withdraw_unbonds the transaction must be signed with the delegator's public key. Note that for delegate, signature verification is also performed because there are tokens debited from the delegator's account.

Slashing

Evidence for byzantine behaviour is received from Tendermint ABCI on BeginBlock. For each evidence:

  • append the evidence into slash/{evidence.validator_address}
  • calculate the slashed amount from deltas in and before the evidence.epoch in validator/{validator_address}/total_deltas for the evidence.validator_address and the slash rate
  • deduct the slashed amount from the validator/{validator_address}/total_deltas at pipeline_length offset
  • update the validator/{validator_address}/voting_power for the evidence.validator_address in and after epoch n + pipeline_length
  • update the total_voting_power in and after epoch n + pipeline_length
  • update validator_set in and after epoch n + pipeline_length

Validity predicate

In the following description, "pre-state" is the state prior to transaction execution and "post-state" is the state posterior to it.

Any changes to PoS epoched data are checked to update the structure as described in epoched data storage.

Because some key changes are expected to relate to others, the VP also accumulates some values that are checked for validity after key specific logic:

  • balance_delta: token::Change
  • bond_delta: HashMap<Address, token::Change>
  • unbond_delta: HashMap<Address, token::Change>
  • total_deltas: HashMap<Address, token::Change>
  • total_stake_by_epoch: HashMap<Epoch, HashMap<Address, token::Amount>>
  • expected_voting_power_by_epoch: HashMap<Epoch, HashMap<Address, VotingPower>>: calculated from the validator's total deltas
  • expected_total_voting_power_delta_by_epoch: HashMap<Epoch, VotingPowerDelta>: calculated from the validator's total deltas
  • voting_power_by_epoch: HashMap<Epoch, <HashMap<Address, VotingPower>>
  • validator_set_pre: Option<ValidatorSets<Address>>
  • validator_set_post: Option<ValidatorSets<Address>>
  • total_voting_power_delta_by_epoch: HashMap<Epoch, VotingPowerDelta>
  • new_validators: HashMap<Address, NewValidator>

The accumulators are initialized to their default values (empty hash maps and hash set). The data keyed by address are using the validator addresses.

For any updated epoched data, the last_update field must be set to the current epoch.

The validity predicate triggers a validation logic based on the storage keys modified by a transaction:

  • validator/{validator_address}/consensus_key:
    match (pre_state, post_state) {
      (None, Some(post)) => {
        // - check that all other required validator fields have been initialized
        // - check that the `state` sub-key for this validator address has been set
        // correctly, i.e. the value should be initialized at `pipeline_length` offset
        // - insert into or update `new_validators` accumulator
      },
      (Some(pre), Some(post)) => {
        // - check that the new consensus key is different from the old consensus
        // key and that it has been set correctly, i.e. the value can only be changed at `pipeline_length` offset
      },
      _ => false,
    }
    
  • validator/{validator_address}/state:
    match (pre_state, post_state) {
      (None, Some(post)) => {
        // - check that all other required validator fields have been initialized
        // - check that the `post` state is set correctly:
        //   - the state should be set to `pending` in the current epoch and `candidate` at pipeline offset
        // - insert into or update `new_validators` accumulator
      },
      (Some(pre), Some(post)) => {
        // - check that a validator has been correctly deactivated or reactivated
        // - the `state` should only be changed at `pipeline_length` offset
        // - if the `state` becomes `inactive`, it must have been `pending` or `candidate`
        // - if the `state` becomes `pending`, it must have been `inactive`
        // - if the `state` becomes `candidate`, it must have been `pending` or `inactive`
      },
      _ => false,
    }
    
  • validator/{validator_address}/total_deltas:
    • find the difference between the pre-state and post-state values and add it to the total_deltas accumulator and update total_stake_by_epoch, expected_voting_power_by_epoch and expected_total_voting_power_delta_by_epoch
  • validator/{validator_address}/voting_power:
    • find the difference between the pre-state and post-state value and insert it into the voting_power_by_epoch accumulator
  • bond/{bond_source}/{bond_validator}/delta:
    • for each difference between the post-state and pre-state values:
      • if the difference is not in epoch n or n + pipeline_length, panic
      • find slashes for the bond_validator, if any, and apply them to the delta value
      • add it to the bond_delta accumulator
  • unbond/{unbond_source}/{unbond_validator}/deltas:
    • for each difference between the post-state and pre-state values:
      • if the difference is not in epoch n or n + unboding_length, panic
      • find slashes for the bond_validator, if any, and apply them to the delta value
      • add it to the unbond_delta accumulator
  • validator_set:
    • set the accumulators validator_set_pre and validator_set_post
  • total_voting_power:
    • find the difference between the post-state and pre-state
    • add it to the total_voting_power_delta_by_epoch accumulator
  • PoS account's balance:
    • find the difference between the post-state and pre-state
    • add it to the balance_delta accumulator

No other storage key changes are permitted by the VP.

After the storage keys iteration, we check the accumulators:

  • For each total_deltas, there must be the same delta value in bond_delta.
  • For each bond_delta, there must be validator's change in total_deltas.
  • Check that all positive unbond_delta also have a total_deltas update. Negative unbond delta is from withdrawing, which removes tokens from unbond, but doesn't affect total deltas.
  • Check validator sets updates against validator total stakes.
  • Check voting power changes against validator total stakes.
  • Check expected voting power changes against voting_power_by_epoch.
  • Check expected total voting power change against total_voting_power_delta_by_epoch.
  • Check that the sum of bonds and unbonds deltas is equal to the balance delta.
  • Check that all the new validators have their required fields set and that they have been added to the validator set

Crypto primitives

Tracking Issue


This page should describe cryptography primitives that we might want to use, such as types of keys, hashing functions, etc.

Actors and Incentives

Anoma consists of various actors fulfilling various roles in the network. They are all incentivized to act for the good of the network. The native Anoma token XAN is used to settle transaction fees and pay for the incentives in Anoma.

Fees associated with a transaction

Users of Anoma can

  • transfer private assets they hold to other users and
  • barter assets with other users.

Each transaction may be associated with the following fees, paid in XAN:

  • Execution fees to compensate for computing, storage and memory costs, charges at 2 stages:
    • initial fee (init_f): charged before the transaction is settled
    • post-execution fee (exe_f): charged after the settlement
  • Exchange fee (ex_f): a fee proportional to the value exchanged in a trade

Actors and their associated fees and responsibilities

ActorResponsibilitiesIncentivesBond in escrowMay also be
UserMake offers or send transactionsFeatures of AnomaXAnyone
SignerGenerate key shardsportions of init_f, exe_fValidator
ValidatorValidateportions of init_f, exe_fSigner
SubmitterSubmit orders & pay init_fsuccessful orders get init_f back plus bonusX
Intent gossip operatorSigns and shares ordersportions of init_f, exe_fX
Market makerSigns and broadcast ordersthe difference between the ask and bid priceX
ProposerProposes blocksportions of init_f, exe_fValidator

Questions to explore:

  • How do we calculate the incentives? What are the equations for each actor?

  • How do we calculate the bond/reward for the signers and validators?

  • How do we ensure certain dual/multi agencies are allowed but not others? E.g., signers can be validators but we may not want them to be proposers because they may have knowledge of which transactions are encrypted.

Actors and fees flowchart

Summary

Proof of Stake (PoS) system

Epoch

An epoch is a range of blocks or time that is defined by the base ledger and made available to the PoS system. This document assumes that epochs are identified by consecutive natural numbers. All the data relevant to PoS are associated with epochs.

Epoched data

Epoched data are data associated with a specific epoch that are set in advance. The data relevant to the PoS system in the ledger's state are epoched. Each data can be uniquely identified. These are:

Changes to the epoched data do not take effect immediately. Instead, changes in epoch n are queued to take effect in the epoch n + pipeline_length for most cases and n + unboding_length for unbonding actions. Should the same validator's data or same bonds (i.e. with the same identity) be updated more than once in the same epoch, the later update overrides the previously queued-up update. For bonds, the token amounts are added up. Once the epoch n has ended, the queued-up updates for epoch n + pipeline_length are final and the values become immutable.

Entities

  • Validator: An account with a public consensus key, which may participate in producing blocks and governance activities. A validator may not also be a delegator.
  • Delegator: An account that delegates some tokens to a validator. A delegator may not also be a validator.

Additionally, any account may submit evidence for a slashable misbehaviour.

Validator

A validator must have a public consensus key. Additionally, it may also specify optional metadata fields (TBA).

A validator may be in one of the following states:

  • inactive: A validator is not being considered for block creation and cannot receive any new delegations.
  • pending: A validator has requested to become a candidate.
  • candidate: A validator is considered for block creation and can receive delegations.

For each validator (in any state), the system also tracks total bonded tokens as a sum of the tokens in their self-bonds and delegated bonds, less any unbonded tokens. The total bonded tokens determine their voting voting power by multiplication by the votes_per_token parameter. The voting power is used for validator selection for block creation and is used in governance related activities.

Validator actions

  • become validator: Any account that is not a validator already and that doesn't have any delegations may request to become a validator. It is required to provide a public consensus key and staking reward address. For the action applied in epoch n, the validator's state will be immediately set to pending, it will be set to candidate for epoch n + pipeline_length and the consensus key is set for epoch n + pipeline_length.
  • deactivate: Only a pending or candidate validator account may deactivate. For this action applied in epoch n, the validator's account is set to become inactive in the epoch n + pipeline_length.
  • reactivate: Only an inactive validator may reactivate. Similarly to become validator action, for this action applied in epoch n, the validator's state will be immediately set to pending and it will be set to candidate for epoch n + pipeline_length.
  • self-bond: A validator may lock-up tokens into a bond only for its own validator's address.
  • unbond: Any self-bonded tokens may be partially or fully unbonded.
  • withdraw unbonds: Unbonded tokens may be withdrawn in or after the unbond's epoch.
  • change consensus key: Set the new consensus key. When applied in epoch n, the key is set for epoch n + pipeline_length.

Active validator set

From all the candidate validators, in each epoch the ones with the most voting power limited up to the max_active_validators parameter are selected for the active validator set. The active validator set selected in epoch n is set for epoch n + pipeline_length.

Delegator

A delegator may have any number of delegations to any number of validators. Delegations are stored in bonds.

Delegator actions

  • delegate: An account which is not a validator may delegate tokens to any number of validators. This will lock-up tokens into a bond.
  • undelegate: Any delegated tokens may be partially or fully unbonded.
  • withdraw unbonds: Unbonded tokens may be withdrawn in or after the unbond's epoch.

Bonds

A bond locks-up tokens from validators' self-bonding and delegators' delegations. For self-bonding, the source address is equal to the validator's address. Only validators can self-bond. For a bond created from a delegation, the bond's source is the delegator's account.

For each epoch, bonds are uniquely identified by the pair of source and validator's addresses. A bond created in epoch n is written into epoch n + pipeline_length. If there already is a bond in the epoch n + pipeline_length for this pair of source and validator's addresses, its tokens are incremented by the newly bonded amount.

Any bonds created in epoch n increment the bond's validator's total bonded tokens by the bond's token amount and update the voting power for epoch n + pipeline_length.

The tokens put into a bond are immediately deducted from the source account.

Unbond

An unbonding action (validator unbond or delegator undelegate) requested by the bond's source account in epoch n creates an "unbond" with epoch set to n + unbounding_length. We also store the epoch of the bond(s) from which the unbond is created in order to determine if the unbond should be slashed if a fault occurred within the range of bond epoch (inclusive) and unbond epoch (exclusive).

Any unbonds created in epoch n decrements the bond's validator's total bonded tokens by the bond's token amount and update the voting power for epoch n + unbonding_length.

An "unbond" with epoch set to n may be withdrawn by the bond's source address in or any time after the epoch n. Once withdrawn, the unbond is deleted and the tokens are credited to the source account.

Staking rewards

To a validator who proposed a block, the system rewards tokens based on the block_proposer_reward system parameter and each validator that voted on a block receives block_vote_reward.

Slashing

Instead of absolute values, validators' total bonded token amounts and bonds' and unbonds' token amounts are stored as their deltas (i.e. the change of quantity from a previous epoch) to allow distinguishing changes for different epoch, which is essential for determining whether tokens should be slashed. However, because slashes for a fault that occurred in epoch n may only be applied before the beginning of epoch n + unbonding_length, in epoch m we can sum all the deltas of total bonded token amounts and bonds and unbond with the same source and validator for epoch equal or less than m - unboding_length into a single total bonded token amount, single bond and single unbond record. This is to keep the total number of total bonded token amounts for a unique validator and bonds and unbonds for a unique pair of source and validator bound to a maximum number (equal to unbonding_length).

To disincentivize validators misbehaviour in the PoS system a validator may be slashed for any fault that it has done. An evidence of misbehaviour may be submitted by any account for a fault that occurred in epoch n anytime before the beginning of epoch n + unbonding_length.

A valid evidence reduces the validator's total bonded token amount by the slash rate in and before the epoch in which the fault occurred. The validator's voting power must also be adjusted to the slashed total bonded token amount. Additionally, a slash is stored with the misbehaving validator's address and the relevant epoch in which the fault occurred. When an unbond is being withdrawn, we first look-up if any slash occurred within the range of epochs in which these were active and if so, reduce its token amount by the slash rate. Note that bonds and unbonds amounts are not slashed until their tokens are withdrawn.

The invariant is that the sum of amounts that may be withdrawn from a misbehaving validator must always add up to the total bonded token amount.

System parameters

The default values that are relative to epoch duration assume that an epoch last about 24 hours.

  • max_validator_slots: Maximum active validators, default 128
  • pipeline_len: Pipeline length in number of epochs, default 2
  • unboding_len: Unbonding duration in number of epochs, default 6
  • votes_per_token: Used in validators' voting power calculation, default 100‱ (1 voting power unit per 1000 tokens)
  • block_proposer_reward: Amount of tokens rewarded to a validator for proposing a block
  • block_vote_reward: Amount of tokens rewarded to each validator that voted on a block proposal
  • duplicate_vote_slash_rate: Portion of validator's stake that should be slashed on a duplicate vote
  • light_client_attack_slash_rate: Portion of validator's stake that should be slashed on a light client attack

Storage

The system parameters are written into the storage to allow for their changes. Additionally, each validator may record a new parameters value under their sub-key that they wish to change to, which would override the systems parameters when more than 2/3 voting power are in agreement on all the parameters values.

The validators' data are keyed by the their addresses, conceptually:

type Validators = HashMap<Address, Validator>;

Epoched data are stored in the following structure:

struct Epoched<Data> {
  /// The epoch in which this data was last updated
  last_update: Epoch,
  /// Dynamically sized vector in which the head is the data for epoch in which 
  /// the `last_update` was performed and every consecutive array element is the
  /// successor epoch of the predecessor array element. For system parameters, 
  /// validator's consensus key and state, `LENGTH = pipeline_length + 1`. 
  /// For all others, `LENGTH = unbonding_length + 1`.
  data: Vec<Option<Data>>
}

Note that not all epochs will have data set, only the ones in which some changes occurred.

To try to look-up a value for Epoched data with independent values in each epoch (such as the active validator set) in the current epoch n:

  1. let index = min(n - last_update, pipeline_length)
  2. read the data field at index:
    1. if there's a value at index return it
    2. else if index == 0, return None
    3. else decrement index and repeat this sub-step from 1.

To look-up a value for Epoched data with delta values in the current epoch n:

  1. let end = min(n - last_update, pipeline_length) + 1
  2. sum all the values that are not None in the 0 .. end range bounded inclusively below and exclusively above

To update a value in Epoched data with independent values in epoch n with value new for epoch m:

  1. let shift = min(n - last_update, pipeline_length)
  2. if shift == 0:
    1. data[m - n] = new
  3. else:
    1. for i in 0 .. shift range bounded inclusively below and exclusively above, set data[i] = None
    2. rotate data left by shift
    3. set data[m - n] = new
    4. set last_update to the current epoch

To update a value in Epoched data with delta values in epoch n with value delta for epoch m:

  1. let shift = min(n - last_update, pipeline_length)
  2. if shift == 0:
    1. set data[m - n] = data[m - n].map_or_else(delta, |last_delta| last_delta + delta) (add the delta to the previous value, if any, otherwise use the delta as the value)
  3. else:
    1. let sum to be equal to the sum of all delta values in the i in 0 .. shift range bounded inclusively below and exclusively above and set data[i] = None
    2. rotate data left by shift
    3. set data[0] = data[0].map_or_else(sum, |last_delta| last_delta + sum)
    4. set data[m - n] = delta
    5. set last_update to the current epoch

The invariants for updates in both cases are that m - n >= 0 and m - n <= pipeline_length.

For the active validator set, we store all the active and inactive validators separately with their respective voting power:

type VotingPower = u64;

/// Validator's address with its voting power.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct WeightedValidator {
  /// The `voting_power` field must be on top, because lexicographic ordering is
  /// based on the top-to-bottom declaration order and in the `ValidatorSet`
  /// the `WeighedValidator`s these need to be sorted by the `voting_power`.
  voting_power: VotingPower,
  address: Address,
}

struct ValidatorSet {
  /// Active validator set with maximum size equal to `max_active_validators`
  active: BTreeSet<WeightedValidator>,
  /// All the other validators that are not active
  inactive: BTreeSet<WeightedValidator>,
}

type ValidatorSets = Epoched<ValidatorSet>;

/// The sum of all active and inactive validators' voting power
type TotalVotingPower = Epoched<VotingPower>;

When any validator's voting power changes, we attempt to perform the following update on the ActiveValidatorSet:

  1. let validator be the validator's address, power_before and power_after be the voting power before and after the change, respectively
  2. let power_delta = power_after - power_before
  3. let min_active = active.first() (active validator with lowest voting power)
  4. let max_inactive = inactive.last() (inactive validator with greatest voting power)
  5. find whether the validator is active, let is_active = power_before >= max_inactive.voting_power
    1. if is_active:
      1. if power_delta > 0 && power_after > max_inactive.voting_power, update the validator in active set with voting_power = power_after
      2. else, remove the validator from active, insert it into inactive and remove max_inactive.address from inactive and insert it into active
    2. else (!is_active):
      1. if power_delta < 0 && power_after < min_active.voting_power, update the validator in inactive set with voting_power = power_after
      2. else, remove the validator from inactive, insert it into active and remove min_active.address from active and insert it into inactive

Within each validator's address space, we store public consensus key, state, total bonded token amount and voting power calculated from the total bonded token amount (even though the voting power is stored in the ValidatorSet, we also need to have the voting_power here because we cannot look it up in the ValidatorSet without iterating the whole set):

struct Validator {
  consensus_key: Epoched<PublicKey>,
  state: Epoched<ValidatorState>,
  total_deltas: Epoched<token::Amount>,
  voting_power: Epoched<VotingPower>,
}

enum ValidatorState {
  Inactive,
  Pending,
  Candidate,
}

The bonds and unbonds are keyed by their identifier:

type Bonds = HashMap<BondId, Epoched<Bond>>;
type Unbonds = HashMap<BondId, Epoched<Unbond>>;

struct BondId {
  validator: Address,
  /// The delegator adddress for delegations, or the same as the `validator`
  /// address for self-bonds.
  source: Address,
}

struct Bond {
  /// A key is a the epoch set for the bond. This is used in unbonding, where
  // it's needed for slash epoch range check.
  deltas: HashMap<Epoch, token::Amount>,
}

struct Unbond {
  /// A key is a pair of the epoch of the bond from which a unbond was created
  /// the epoch of unboding. This is needed for slash epoch range check.
  deltas: HashMap<(Epoch, Epoch), token::Amount>
}

For slashes, we store the epoch and block height at which the fault occurred, slash rate and the slash type:

struct Slash {
  epoch: Epoch,
  block_height: u64,
  /// slash token amount ‱ (per ten thousand)
  rate: u8,
  r#type: SlashType,
}

Initialization

An initial validator set with self-bonded token amounts must be given on system initialization.

This set is used to pre-compute epochs in the genesis block from epoch 0 to epoch pipeline_length - 1.

Prototypes

A prototype should start with a description of its goals. These can include, but are not limited to a proof of concept of novel ideas or alternative approaches, comparing different libraries and gathering feedback.

To get started on a prototype, please:

  • open an issue on this repository
  • add a sub-page to this section with a link to the issue

The page outlines the goals and possibly contains any notes that are not suitable to be added to the prototype source itself, while the issue should track the sub-task, their progress, and assignees.

The code quality is of lesser importance in prototypes. To put the main focus on the prototype's goals, we don't need to worry much about testing, linting and doc strings.

Advancing a successful prototype

Once the goals of the prototype have been completed, we can assess if we'd like to advance the prototype to a development version.

In order to advance a prototype, in general we'll want to:

  • review & clean-up the code for lint, format and best practices
  • enable common Rust lints
  • review any new dependencies
  • add docs for any public interface (internally public too)
  • add automated tests
  • if the prototype has diverged from the original design, update these pages

Base ledger prototype

Version 2

tracking issue https://github.com/anoma/anoma/issues/62

Goals

  • storage
    • build key schema for access
    • implement dynamic account sub-spaces
  • implement more complete support for WASM transactions and validity predicates
    • transactions can read/write all storage
    • validity predicates receive the set of changes (changed keys or complete write log) and can read their pre/post state
  • add basic transaction gas metering
  • various other improvements

Version 1

tracking issue https://github.com/heliaxdev/rd-pm/issues/5

Goals

  • get some hands-on experience with Rust and Tendermint
  • initial usable node + client (+ validator?) setup
  • provide a base layer for other prototypes that need to build on top of a ledger

Components

The main components are built in a single Cargo project with shared library code and multiple binaries:

  • anoma - main executable with commands for both the node and the client (anoma node and anoma client)
  • anoman - the node
  • anomac - the client

Node

The node is built into anoman.

Shell

The shell is what currently pulls together all the other components in the node.

When it's ran:

  • establish a channel (e.g.mpsc::channel - Multi-producer, single-consumer FIFO queue) for communication from tendermint to the shell
  • launch tendermint node in another thread with the channel sender
    • send tendermint ABCI requests via the channel together with a new channel sender to receive a response
  • run shell loop with the channel receiver, which handles ABIC requests:
Tendermint

This module handles initializing and running tendermint and forwards messages for the ABCI requests via its channel sender.

Storage

Key-value storage. More details are specified on Storage page.

CLI
  • anoma run to start the node (will initialize (if needed) and launch tendermint under the hood)
  • anoma reset to delete all the node's state from DB and tendermint's state

Client

Allows to submit a transaction with an attached wasm code to the node with:

anoma tx --code tx.wasm

It presents back the received response on stdout. Currently, it waits for both the mempool validation and application in a block.

Shared

Config

Configuration settings:

  • home directory (db storage and tendermint config and data)
Genesis

The genesis parameters, such as the initial validator set, are used to initialize a chain's genesis block.

RPC types

The types for data that can be submitted to the node via the client's RPC commands.

Intent Gossip system prototype

tracking issue https://github.com/anoma/anoma/issues/35

Goals

  • learning rust
  • usable node + client setup :
    • intent
    • incentive function
    • mempool and white list
  • basic matchmaker

components

The intent gossip is build conjointly to the ledger and share the same binary.

Node

The node is built into anoman, it runs all the necesarry part, rpc server, libp2p, intent gossip app.

Intent gossip application

The intent gossip application

Mempool
Filter

Network behaviour

The network behaviour is the part that react on network event. It creates a channel (e.g. tokio::mpsc::channel) with the intent gossip to communicate all intent it receive.

Rpc server

If the rpc command line option is set it creates a tonic server that receive command from a client and send theses through a channel (e.g. tokio::mpsc::channel) to the the intent gossip.

Client

Allow to submit a intent : anoma gossip --data "data"

Libraries & Tools

The aim of this section is to document possible choices for certain components. For components where many choices are available, such as a database backend, an overview of the important differences of the considered libraries may be given.

network

Libp2p : Peer To Peer network

https://github.com/libp2p/rust-libp2p

peer-to-peer framework that takes care of the transport/identity and message encryption for us.

tonic : Client/Server with protobuf (prost)

https://github.com/hyperium/tonic

Generates a client/server from protobuf file. This can be used for a rpc server.

network behaviour

Gossipsub

https://github.com/libp2p/specs/tree/master/pubsub/gossipsub

Publish/Subscribe protocol, improvement over floodsub.

Command-line interface

Important factors:

  • UX
  • ease of use
  • cross-platform

The considered libraries:

  • clap

Clap

https://github.com/clap-rs/clap

Probably the most widely used CLI library in Rust.

With version 2.x, we'd probably want to use it with Structops for deriving.

But we can probably use 3.0, which is not yet stable, but is pretty close https://github.com/clap-rs/clap/issues/1037. This version comes with deriving attributes and also other new ways to build CLI commands.

Database

Important factors:

  • persistent key/value storage
  • reliability and efficiency (runtime performance and disk usage)
  • thread safety
  • ease of use

The considered DBs:

  • LMDB
  • LevelDB
  • RocksDB
  • sled - Rust native

To watch:

The current preference is for RocksDB as it's tried and tested. Eventually, we might want to benchmark against other backends for our specific use case.

LMDB

https://symas.com/lmdb/

A compact and efficient, persistent in-memory (i.e. mmap-based) B+trees database. Reportedly has a great read performance, but not as good at writing.

Rust bindings:

LevelDB

Log Structured Merge Tree db. Uses one global lock. Better write performance than LMDB and lower DB size.

Rust bindings:

RocksDB

A fork of LevelDB with different optimizations (supposedly for RAM and flash storage).

Used in https://github.com/simplestaking/tezedge and https://github.com/near/nearcore.

Rust bindings:

Sled

Repo: https://github.com/spacejam/sled Homepage: https://sled.rs/

Modern, zero-copy reads, lock-free and many more features.


Merkle tree data structure

Some popular choices for merkle tree in the industry are AVL(+) tree, Patricia Trie and Sparse Merkle Tree, each with different trade-offs.

AVL(+) tree is used in e.g. Cosmos. The advantage of this structure is that key don't need to be hashed prior to insertion/look-up.

Patricia trie used in e.g. Ethereum and Plebeia for Tezos are designed to be more space efficient.

Sparse Merle tree as described in Optimizing sparse Merkle trees used in e.g. Plasma Cash are somewhat similar to Patricia trees, but perhaps conceptually simpler.

Considered libraries:

  • merk
  • sparse-merkle-tree
  • patricia_tree

merk

https://github.com/nomic-io/merk

Using AVL tree built on top of RocksDB. It makes it easy to setup Merkle tree storage, but:

sparse-merkle-tree

https://github.com/jjyr/sparse-merkle-tree

A nice abstraction, albeit not yet declared stable. It allows to plug-in a custom hasher function (which is important for circuit friendliness) and storage backend. Has minimal dependencies and support Rust no_std.

patricia_tree

https://github.com/sile/patricia_tree

Logging

Options to consider:

  • env_logger
  • slog
  • tracing

The current preference is for tracing in combination with tracing-subscriber (to log collected events and traces), because we have some async and parallelized code. In future, we should also add tracing-appender for rolling file logging.

Env_logger

https://github.com/env-logger-rs/env_logger/

A simple logger used by many Rust tools, configurable by env vars. Usually combined with pretty-env-logger.

Slog

https://github.com/slog-rs/slog

Composable, structured logger. Many extra libraries with extra functionality, e.g.:

Tracing

https://github.com/tokio-rs/tracing

Tracing & logging better suited for concurrent processes and async code. Many extra libraries with extra functionality, e.g.:

Packaging

For Rust native code, cargo works great, but we'll need to package stuff from outside of Rust too (e.g. tendermint). The goal is to have a repo that can always build as is (reproducible) and easily portable (having a single command to install all the deps).

Options to consider:

Cargo

For Rust dependencies, it would be nice to integrate and use:

Nix

Purely functional package management for reproducible environment. The big drawback is its language.

Guix

Similar package management capability to nix, but using scheme language.

Docker

Not ideal for development, but we'll probably want to provide docker images for users.

Serialization libraries

Because the serialization for the RPC and storage have different priorities, it might be beneficial to use a different library for each.

RPC

Important factors:

  • security, e.g.:
    • handling of malicious input (buffers should not be trusted)
    • secure RPC, if included (e.g. DoS or memory exhaustion vulnerabilities)
  • native and cross-language adoption for easy interop
  • ease of use
  • reasonable performance

The considered libraries:

  • protobuf
  • cap'n'proto
  • flatbuffers
  • serde

The current preference is for protobuf using the prost library.

Storage

Important factors:

  • consistent binary representation for hashing
  • preserve ordering (for DB keys)
  • ease of use
  • reasonable performance

The considered libraries:

  • bincode
  • borsh

Protobuf

The most mature and widely adopted option. Usually combined with gRPC framework. The Tendermint Rust ABCI provides protobuf definitions.

Implementations:

A comparison of the two main competing Rust implementations seems to favor Prost. Prost reportedly generates cleaner (more idiomatic) Rust code (https://hacks.mozilla.org/2019/04/crossing-the-rust-ffi-frontier-with-protocol-buffers/#comment-24671). Prost also has better performance (https://github.com/danburkert/prost/issues/398#issuecomment-751600653). It is possible to also add serde derive attributes for e.g. JSON support. JSON can be useful for development, requests inspection and web integration. However, to reduce attack surface, we might want to disallow JSON for write requests on mainnet by default.

gRPC implementations:

Cap'n'proto

It avoids serialization altogether, you use the data natively in a representation that is efficient for interchange ("zero-copy"). The other cool feature is its "time-traveling RPC". On the other hand concern for this lib is a much lower adoption rate, especially the Rust port which is not as complete. The format is designed to be safe against malicious input (on the both sides of a communication channel), but according to FAQ the reference impl (C++) has not yet undergone security review.

Implementations:

Flatbuffers

Similar to protobuf, but zero-copy like Cap'n'proto, hence a lot faster.

Unfortunately, the Rust implementation is lacking buffer verifiers, which is crucial for handling malicious requests gracefully. There is only draft implementation https://github.com/google/flatbuffers/pull/6269. This most likely rules out this option.

Implementations:

Serde

Serde is Rust native framework with great ergonomics. It supports many different formats implemented as libraries. It's used in some DBs too. Serde itself gives no security guarantees, handling of malicious input depends heavily on the used format. Serde can be used in combination with many other formats, like protobuf.

Bincode

https://github.com/servo/bincode

Built on top of serde. Easy to use.

Borsh

https://github.com/near/borsh-rs

Used in the Near protocol, it guarantees consistent representations and has a specification. It is also faster than bincode and is being implemented in other languages.

WASM runtime

Considered runtimes:

  • wasmer
  • wasmi

A good comparison overview is given in this thread that discusses replacing wasmi with wasmer and its links. In summary:

  • wasmer has native rust closures (simpler code)
  • wasmer uses lexical scoping to import functions, wasmi is based on structs and trait impls
  • the wasmer org maintains wasmer packages in many languages
  • wasmer may be vulnerable to compiler bombs
  • gas metering
    • wasmi inject calls to the host gas meter from Wasm modules
    • wasmer
      • uses Middleware which injects the instructions at the parsing stage of the compiler (with inlining) - reduced overhead
      • must also consider compiler gas cost and how to handle compiler performance changes
    • it's hard to implement gas rules for precompiles
  • nondeterminism concerns
    • different wasm versions (e.g. newly added features) have to be handled in both the compiled and interpreted versions
    • non-determinism in the source language cannot be made deterministic in complied/interpreted wasm either
    • threading - look like it has a long way to go before being usable
    • floats/NaN - can be avoided https://github.com/WebAssembly/design/issues/582#issuecomment-191318866
    • SIMD
    • environment resources exhaustion
  • both are using the same spec, in wasmi words "there shouldn't be a problem migrating to another spec compliant execution engine." and "wasmi should be a good option for initial prototyping"
    • of course this is only true if we don't use features that are not yet in the spec

wasmer

Repo: https://github.com/wasmerio/wasmer

Compiled with multiple backends (Singlepass, Cranelift and LLVM). It support metering via a Middleware.

wasmi

Repo: https://github.com/paritytech/wasmi

Built for blockchain to ensure high degree of correctness (security, determinism). Interpreted, hence slower.

Error handling

The current preference is to use thiserror for most code and eyre for reporting errors at the CLI level and the client.

To make the code robust, we should avoid using code that may panic for errors that recoverable and handle all possible errors explicitly. Two exceptions to this rule are:

  • prototyping, where it's fine to use unwrap, expect, etc.
  • in code paths with conditional compilation only for development build, where it's preferable to use expect in place of unwrap to help with debugging

In case of panics, we should provide an error trace that is helpful for trouble-shooting and debugging.

A great post on error handling library/application distinction: https://nick.groenen.me/posts/rust-error-handling/.

The considered DBs:

  • thiserror
  • anyhow
  • eyre

The current preference is to use eyre at the outermost modules to print any encountered errors nicely back to the user and thiserror elsewhere.

Thiserror

Macros for user-derived error types. Commonly used for library code.

Anyhow

Easy error handling helpers. Commonly used for application code.

Eyre

Fork of anyhow with custom error reporting.

Glossary

  • intent gossip The intent gossip network must maintain a mempool of intents and gossips them via a p2p layer. Each intent gossip node maintains a list of interests that describe what intents it is interested in.
  • intent An expression of intent describes a particular trade an account agrees to.
  • matchmaker The matchmaker tries to match intents together. For each match it crafts a valid transaction and submits it to the base ledger.
  • validity predicate (VP) A validity predicate is a piece of code attached to an account that can accept or reject any state changes performed by a transaction in its sub-space.

Resources

Please add anything relevant to the project that you'd like to share with others, such as research papers, blog posts or tutorials. If it's not obvious from the title, please add some description.

General

Rust

IDE

VsCode

Some handy extensions (output of code --list-extensions):

aaron-bond.better-comments
be5invis.toml
bodil.file-browser
bungcip.better-toml
DavidAnson.vscode-markdownlint
jacobdufault.fuzzy-search
kahole.magit
matklad.rust-analyzer
oderwat.indent-rainbow
# easy to see if crates are up-to-date and update if not
serayuzgur.crates
streetsidesoftware.code-spell-checker
vscodevim.vim
# this is like https://www.spacemacs.org/ but in VsCode
VSpaceCode.vspacecode
VSpaceCode.whichkey
# org-mode
vscode-org-mode.org-mode
publicus.org-checkbox

Add these to your settings.json to get rustfmt and clippy with the nightly version that we use:

"rust-analyzer.checkOnSave.overrideCommand": [
    "cargo",
    "+nightly-2021-08-04",
    "clippy",
    "--workspace",
    "--message-format=json",
    "--all-targets"
],
"rust-analyzer.rustfmt.overrideCommand": [
    "rustup",
    "run",
    "nightly-2021-08-04",
    "--",
    "rustfmt",
    "--edition",
    "2018",
    "--"
],

When editing the wasms source (i.e. wasm/wasm_source/src/..), open the wasm/wasm_source as a workspace to get rust-analyzer working (because the crate is excluded from the root cargo workspace) and then active --all-features for it in the preferences.

Emacs

two main mode:

  • rust-mode official mode supported by rust dev
  • rustic-mode forked with more option and better integration/default value

config example with rustic and use-package

    ;; all flycheck not mandatory not mandatory
  (use-package flycheck
    :commands flycheck-mode
    :init (global-flycheck-mode))

  (use-package flycheck-color-mode-line
    :after flycheck
    :hook
    (flycheck-mode . flycheck-color-mode-line-mode))

  (use-package flycheck-pos-tip
    :after flycheck)
  (use-package lsp-mode
    :after flycheck
    :bind-keymap
    ("C-c i" .  lsp-command-map)
    :hook
    (lsp-mode . lsp-enable-which-key-integration) ;; if wichkey installed
    :commands (lsp lsp-deferred)
    :custom
    (lsp-eldoc-render-all t)
    (lsp-idle-delay 0.3)
    )

  (use-package lsp-ui
    :after lsp-mode
    :commands lsp-ui-mode
    :custom
    (lsp-ui-peek-always-show t)
    (lsp-ui-sideline-show-hover t)
    (lsp-ui-doc-enable nil)
    (lsp-ui-doc-max-height 30)
    :hook (lsp-mode . lsp-ui-mode))

    ;; if ivy installed installed
  (use-package lsp-ivy
    :after lsp-mode ivy
    :commands lsp-ivy-workspace-symbol)

    ;; if company installed
  (use-package company-lsp
    :after lsp-mode company
    :init
    (push 'company-lsp company-backend))

  (use-package rustic
    :bind (:map rustic-mode-map
                ("M-j" . lsp-ui-imenu)
                ("M-?" . lsp-find-references)
                ("C-c C-c ?" . lsp-describe-thing-at-point)
                ("C-c C-c !" . lsp-execute-code-action)
                ("C-c C-c r" . lsp-rename)
                ("C-c C-c TAB" . lsp-rust-analyzer-expand-macro)
                ("C-c C-c q" . lsp-workspace-restart)
                ("C-c C-c Q" . lsp-workspace-shutdown)
                ("C-c C-c s" . lsp-rust-analyzer-status)
                ("C-c C-c C-a" . rustic-cargo-add)
                ("C-c C-c C-d" . rustic-cargo-rm)
                ("C-c C-c C-u" . rustic-cargo-upgrade)
                ("C-c C-c C-u" . rustic-cargo-outdated))
    :hook
    (rustic-mode . lsp-deferred)
    :custom
    (lsp-rust-analyzer-cargo-watch-command "clippy")
    :config
    (rustic-doc-mode t)
  )

Specifications

Anoma is a sovereign, proof-of-stake blockchain protocol that enables private, asset-agnostic cash and private bartering among any number of parties.

This specification defines the Anoma ledger's protocol and its components and the intent gossip and matchmaking system.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC-2119.

Overview

At a high level, Anoma is composed of two main components: the distributed ledger and the intent gossip / matchmaking system. While they are designed to complement each other, they can be operated separately.

The ledger

The ledger is a distributed state machine, relying on functionality provided by Tendermint such as its BFT consensus algorithm with instant finality, P2P networking capabilities, transaction mempool and more. The ledger state machine is built on top the ABCI.

For block validator voting power assignment, the ledger employs a proof-of-stake system.

The ledger's key-value storage is organized into blocks and user specific state is organized into accounts. The state machine executes transactions, which can apply arbitrary changes to the state that are validated by validity predicates associated with the accounts involved in the transaction.

To prevent transaction front-running, the ledger employs a DKG scheme as implemented in Ferveo. Using this scheme, transactions are encrypted before being submitted to the ledger. The encrypted transactions are committed by a block proposer to a specific order in which they must be executed once decrypted.

  • TODO add fractal scaling & protocol upgrade system overview

The intent gossip with matchmaking system

  • TODO add an overview

The ledger

The protocol

  • TODO describe DKG transactions
  • TODO DKG transactions will include replay protection (this is because we can simply check a counter against the source (i.e. gas payer) of the transaction before the transactions order is committed to by the DKG protocol, which could affect the expected counter order for sources with multiple queued transactions)

Transactions

A transaction encoded with proto3 received from ABCI DeliverTx method is executed in two main steps:

  1. Transaction execution
  2. Validity predicates check

Transaction execution

For any error encountered in any of the following steps of transaction execution, the protocol MUST charge the gas used by the transaction and discard any storage changes that the transaction attempted to perform.

  1. Charge a base transaction gas: \( \verb|BASE_TRANSACTION_FEE| \)
  2. Decode the transaction bytes and validate the data. The field timestamp is required.
  3. Charge WASM compilation gas, proportional to the bytes length of the code field of the transaction (this is because the WASM code is compiled with a single-pass compiler): \( \verb|length| * \verb|COMPILE_GAS_PER_BYTE| \)
  4. Validate the WASM code from the code field of the transaction.
  5. Inject a gas counter into the code.
  6. Inject a stack height limiter into the code.
  7. Compile the transaction code with a single-pass compiler (for example, the Wasmer runtime single-pass compiler). The compilation computational complexity MUST be linear in proportion to the code size.
  8. Initialize the WASM linear memory with descriptor having the initial memory size equal to TX_MEMORY_INIT_PAGES and maximum memory size to TX_MEMORY_MAX_PAGES.
  9. Instantiate the WASM module with imported transaction host environment functions and the instantiated WASM memory.
  10. Write the transaction's data into the memory exported from the WASM module instance.
  11. Attempt to call the module's entrypoint function. The entrypoint MUST have signature:
    func (param i64 i64)
    
    The first argument is the offset to the data input written into the memory and the second argument is its bytes length.

If the transaction executed successfully, it is followed Validity predicates check.

Validity predicates check

For the transaction to be valid, all the triggered validity predicates must accept it.

First, the addresses whose validity predicates should be triggered by the transaction are determined. In this process, the addresses get associated with a set of modified storage keys that are relevant to the address:

  1. The addresses set by the transaction (see insert_verifier in transaction host environment functions) are associated with all the modified storage keys.

    TODO - https://github.com/anoma/anoma/issues/292

  2. The storage keys that were modified by the transaction are associated with the addresses included in the storage keys. Note that a storage key may contain more than one address, in which case all its addresses are associated with this key.

  3. All these addresses are additionally associated with the storage key to the validity predicates of any newly initialized accounts' by the transaction (see init_account in transaction host environment functions).

For all these addresses, attempt to read their validity predicate WASM code from the storage. For each validity predicate look-up, charge storage read gas and WASM compilation gas, proportional to the bytes length of the validity predicate. If any of the validity predicates look-ups fails, or any validity rejects the transaction or fails anywhere in the execution, the whole transaction is rejected. If the transaction is rejected, the protocol MUST charge the gas used by the transaction and discard any storage changes that the transaction attempted to perform.

Execute all validity predicates in parallel as follows:

  1. Charge WASM compilation gas, proportional to the bytes length of the validity predicate (same as for the transaction, WASM code is compiled with a single-pass compiler).
  2. Charge WASM compilation gas, proportional to the bytes length of the validity predicate (same as for the transaction, WASM code is compiled with a single-pass compiler): \( \verb|length| * \verb|COMPILE_GAS_PER_BYTE| \).
  3. Validate the WASM code of the validity predicate.
  4. Inject a gas counter into the code.
  5. Inject a stack height limiter into the code.
  6. Compile the validity predicate with single-pass compiler. The compilation computational complexity MUST be linear in proportion to its bytes size.
  7. Initialize the WASM linear memory with descriptor having the initial memory size equal to VP_MEMORY_INIT_PAGES and maximum memory size to VP_MEMORY_MAX_PAGES.
  8. Instantiate the WASM module with imported validity predicate host environment functions and the instantiated WASM memory.
  9. Write the address of the validity predicate’s owner, the transaction data, the modified storage keys encoded with Borsh, and all the triggered validity predicates owners' addresses encoded with Borsh into the memory exported from the WASM module instance.
  10. Attempt to call the module's entrypoint function. The entrypoint MUST have signature:
    func (param i64 i64 i64 i64 i64 i64 i64 i64) (result i64))
    
    • The first argument is the offset to the owner’s address written into the memory, the second argument is its bytes length
    • The third is the offset of the transaction’s data and fourth is it’s bytes length
    • The fifth is the offset of the modified storage keys and sixth is its bytes length
    • The seventh is the offset of the triggered validity predicates owners' addresses and eighth is its bytes length

Gas

Gas constants

The gas constants are currently chosen arbitrarily and are subject to change following gas accounting estimations.

NameValue
COMPILE_GAS_PER_BYTE1
BASE_TRANSACTION_FEE2
PARALLEL_GAS_DIVIDER10
MIN_STORAGE_GAS1
  • TODO describe gas accounting, wasm gas counter, limits, what happens if we go over limits and how gas relates to fees

WebAssembly (WASM)

WASM constants
NameUnitValue
PAGE (as defined in the WASM spec)kiB64
TX_MEMORY_INIT_PAGESnumber of PAGEs100
TX_MEMORY_MAX_PAGESnumber of PAGEs200
VP_MEMORY_INIT_PAGESnumber of PAGEs100
VP_MEMORY_MAX_PAGESnumber of PAGEs200
WASM_STACK_LIMITstack depth65535

The WASM instantiation, the types, instructions, validation and execution of WASM modules MUST conform to the WebAssembly specification.

WASM validation

The WebAssembly code is REQUIRED to only use deterministic instructions. Furthermore, it MUST NOT use features from any of the following WebAssembly proposals:

  • The reference types proposal
  • The multi-value proposal
  • The bulk memory operations proposal
  • The module linking proposal
  • The SIMD proposal
  • The threads proposal
  • The tail-call proposal
  • The multi memory proposal
  • The exception handling proposal
  • The memory64 proposal
Stack height limiter

To make stack overflows deterministic, set the upper bound of the stack size to WASM_STACK_LIMIT. If the stack height exceeds the limit then execution MUST abort.

WASM memory
  • TODO memory read/write gas costs
Transaction host environment functions

The following functions from the host ledger are made available in transaction's WASM code. They MAY be imported in the WASM module as shown bellow and MUST be provided by the ledger's WASM runtime:

(import "env" "gas" (func (param i32)))
(import "env" "anoma_tx_read" (func (param i64 i64) (result i64)))
(import "env" "anoma_tx_result_buffer" (func (param i64)))
(import "env" "anoma_tx_has_key" (func (param i64 i64) (result i64)))
(import "env" "anoma_tx_write" (func (param i64 i64 i64 i64)))
(import "env" "anoma_tx_delete" (func (param i64 i64)))
(import "env" "anoma_tx_iter_prefix" (func (param i64 i64) (result i64)))
(import "env" "anoma_tx_iter_next" (func (param i64) (result i64)))
(import "env" "anoma_tx_insert_verifier" (func (param i64 i64)))
(import "env" "anoma_tx_update_validity_predicate" (func (param i64 i64 i64 i64)))
(import "env" "anoma_tx_init_account" (func (param i64 i64 i64)))
(import "env" "anoma_tx_get_chain_id" (func (param i64)))
(import "env" "anoma_tx_get_block_height" (func (param ) (result i64)))
(import "env" "anoma_tx_get_block_hash" (func (param i64)))
(import "env" "anoma_tx_log_string" (func (param i64 i64)))

Additionally, the WASM module MUST export its memory as shown:

(export "memory" (memory 0))
  • anoma_tx_init_account TODO newly created accounts' validity predicates aren't used until the block is committed (i.e. only the transaction that created the account may write into its storage in the block in which its being applied).
  • TODO describe functions in detail
Validity predicate host environment functions

The following functions from the host ledger are made available in validity predicate's WASM code. They MAY be imported in the WASM module as shown bellow and MUST be provided by the ledger's WASM runtime.

(import "env" "gas" (func (param i32)))
(import "env" "anoma_vp_read_pre" (func (param i64 i64) (result i64)))
(import "env" "anoma_vp_read_post" (func (param i64 i64) (result i64)))
(import "env" "anoma_vp_result_buffer" (func (param i64)))
(import "env" "anoma_vp_has_key_pre" (func (param i64 i64) (result i64)))
(import "env" "anoma_vp_has_key_post" (func (param i64 i64) (result i64)))
(import "env" "anoma_vp_iter_prefix" (func (param i64 i64) (result i64)))
(import "env" "anoma_vp_iter_pre_next" (func (param i64) (result i64)))
(import "env" "anoma_vp_iter_post_next" (func (param i64) (result i64)))
(import "env" "anoma_vp_get_chain_id" (func (param i64)))
(import "env" "anoma_vp_get_block_height" (func (param ) (result i64)))
(import "env" "anoma_vp_get_block_hash" (func (param i64)))
(import "env" "anoma_vp_verify_tx_signature" (func (param i64 i64 i64 i64) (result i64)))
(import "env" "anoma_vp_eval" (func (param i64 i64 i64 i64) (result i64)))
  • TODO describe functions in detail

Additionally, the WASM module MUST export its memory as shown:

(export "memory" (memory 0))

Storage

  • TODO

Encoding

All the data fields are REQUIRED, unless specified otherwise.

The ledger

Transactions

Transactions MUST be encoded using proto3 in the format as defined for message Tx.

NameTypeDescriptionField Number
codebytesTransaction WASM code.1
dataoptional bytesTransaction data (OPTIONAL).2
timestampgoogle.protobuf.TimestampTimestamp of when the transaction was created.3

Proto definitions

syntax = "proto3";

import "google/protobuf/timestamp.proto";

package types;

message Tx {
  bytes code = 1;
  // TODO this optional is useless because it's default on proto3
  optional bytes data = 2;
  google.protobuf.Timestamp timestamp = 3;
}

message Intent {
  bytes data = 1;
  google.protobuf.Timestamp timestamp = 2;
}

message IntentGossipMessage{
  // TODO remove oneof because it's not used so far
  oneof msg {
    Intent intent = 1;
  }
}

message Dkg {
  string data = 1;
}

message DkgGossipMessage{
  oneof dkg_message {
    Dkg dkg = 1;
  }
}

Archive

Deprecated pages archived for possible later re-use.

Domain name addresses

The transparent addresses are similar to domain names and the ones used in e.g. ENS as specified in EIP-137 and account IDs in Near protocol. These are the addresses of accounts associated with dynamic storage sub-spaces, where the address of the account is the prefix key segment of its sub-space.

A transparent address is a human-readable string very similar to a domain name, containing only alpha-numeric ASCII characters, hyphen (-) and full stop (.) as a separator between the "labels" of the address. The letter case is not significant and any upper case letters are converted to lower case. The last label of an address is said to be the top-level name and each predecessor segment is the sub-name of its successor.

The length of an address must be at least 3 characters. For compatibility with a legacy DNS TXT record, we'll use syntax as defined in RFC-1034 - section 3.5 DNS preferred name syntax. That is, the upper limit is 255 characters and 63 for each label in an address (which should be sufficient anyway); and the label must not begin or end with hyphen (-) and must not begin with a digit.

These addresses can be chosen by users who wish to initialize a new account, following these rules:

  • a new address must be initialized on-chain
    • each sub-label must be authorized by the predecessor level address (e.g. initializing address free.eth must be authorized by eth, or gives.free.eth by free.eth, etc.)
    • note that besides the address creation, each address level is considered to be a distinct address with its own dynamic storage sub-space and validity predicate.
  • the top-level names under certain length (to be specified) cannot be initialized directly, they may be auctioned like in ENS registrar as described in EIP-162.
    • some top-level names may be reserved

For convenience, the anoma top-level address is initially setup to allow initialization of any previously unused second-level address, e.g. bob.anoma (we may want to revise this before launch to e.g. auction the short ones, like with top-level names to make the process fairer).

Like in ENS, the addresses are stored on chain by their hash, encoded with bech32m (not yet adopted in Zcash), which is an improved version of bech32. Likewise, this is for two reasons:

  • help preserve privacy of addresses that were not revealed publicly and to prevent trivial enumeration of registered names (of course, you can still try to enumerate by hashes)
  • using fixed-length string in the ledger simplifies gas accounting