An Overview of Anoma’s Architecture

Awa Sun Yin
Anoma | Intent-centric Architecture
12 min readMar 16, 2022

--

Anoma’s fractal worldview

For up-to-date articles on Anoma, go to anoma.net/blog

Foreword

Lately I’ve been chatting with many folks who are new to Anoma and want to learn more about the project. In most cases, I felt the need to start with a quick overview of the architecture before jumping into example applications or use cases. My goal is that this article is used as a reference to answer the questions “how does Anoma work?”, “why is it fundamentally different than other protocols?”, or “what can I do with Anoma which is impossible with any other protocol?”.

Introduction

The Anoma protocol is designed as a universal coordination mechanism that facilitates all forms of coordination, no matter how simple or complex they are or how few or many number of parties they involve (from 1 to n). As the architecture of the Anoma protocol is unprecedented in many ways, this article provides a human-readable overview of the architecture and why it was designed this way.

Intents

On Anoma, there is a concept called intent. You can think of intents as partial or unfinalized transactions that can be created by anyone. They are messages that have been crafted and signed by a user that express a specific want or preference. Intents can be anything, as simple as a plain human-readable message, a set of characteristics or constraints, or even a complex program. Thus, intents are able to carry economic value and constitute a binding expression of preference, a user commits to a particular action (agreeing to a state transition) in particular circumstances (other state transitions / current state).

Here are the commands that write example intents from Albert, Bertha, and Christel to a file:

echo ‘[{“addr”:”’$ALBERT’”,”key”:”’$ALBERT’”, “max_sell”:”10",”min_buy”:”1", “buy”:”’$veal’”,”sell”:”’$potato’”}]’> intent.A.dataecho ‘[{“addr”:”’$BERTHA’”,”key”:”’$BERTHA’”,”max_sell”:”1",”min_buy”:”5", “token_buy”:”’$apple’”,”token_sell”:”’$veal’”}]’ > intent.B.dataecho '[{"addr":"'$CHRISTEL'","key":"'$CHRISTEL'","max_sell":"5","min_buy":"10", "token_buy":"'$potato'","token_sell":"'$apple'"}]' > intent.C.data

Note that intents are just signed messages and the end user could create them via different interfaces, from command-line tools to web or native user interfaces.

Intent gossip nodes

Once an intent has been crafted it can be submitted to an intent gossip node, which are a peculiar component of Anoma’s peer-to-peer layer. Intent gossip nodes can be operated by anyone, including the end user who crafts the intent. The operators of the intent gossip nodes form pools of intents (there might not be a global pool of intents, as that’d require perfect synchrony among all gossip node operators) that contains any intent that users have submitted to at least one gossip node. The design of the intent gossip node protocol could include a fee or reward sharing mechanism among all gossip nodes that participated in the gossip of an intent that later on got successfully settled — this could be defined as an intent gossip standard, but in the end it is defined by the intent gossip operators (or by the users that want to include it in their intents).

Matchmakers & transaction crafting

Matchmakers are another distinct role on the network. They are actively monitoring the pool of intents with the purpose of finding as many intents as possible that, in aggregate, satisfy the expressed preferences in each intent when combined. To make the role of the matchmaker more tangible, you could think of a very basic matchmaker as a person that manually checks on every intent and determines which ones can be combined via mental calculus. If we translate the example intents above to words:

  • Albert wants to sell at most 10 $potato and buy at least one $veal
  • Bertha wants to sell at most 1 $veal and buy at least 5 $apple
  • Christel wants to sell at most 5 $apple and buy at least 10 $potato

Notice that no intent has a direct match or double-coincidence of wants. But a crafty matchmaker could spot that these three particular intents would satisfy each other when combined.

The sophistication of matchmakers’ methods is unbound: they could be a coordinated group that deploys proprietary optimization algorithms, operate specialized hardware infrastructure in a physically advantageous location, or they could even be a form of artificial intelligence– after all, matchmakers are competing with each other and could do anything to gain an edge.

When a group of combinable intents is found, the matchmaker aggregates them and crafts a transaction which can be submitted to any full node.

Here’s an example of a very basic matchmaker program implemented in Rust:

/// Try to find matching intents in the graph. If found, returns the tx bytes
/// and a hash set of the matched intent IDs.
fn try_match(
graph: &mut DiGraph<ExchangeNode, Address>,
) -> Option<(Vec<u8>, HashSet<Vec<u8>>)> {
// We only use the first found cycle, because an intent cannot be matched
// into more than one tx
if let Some(mut matchned_intents_indices) =
petgraph::algo::tarjan_scc(&*graph).into_iter().next()
{
// a node is a cycle with itself
if matchned_intents_indices.len() > 1 {
println!("found a match: {:?}", matchned_intents_indices);
// Must be sorted in reverse order because it removes the node by
// index otherwise it would not remove the correct node
matchned_intents_indices.sort_by(|a, b| b.cmp(a));
if let Some(tx_data) =
prepare_tx_data(graph, &matchned_intents_indices)
{
let removed_intent_ids = matchned_intents_indices
.into_iter()
.filter_map(|i| {
if let Some(removed) = graph.remove_node(i) {
Some(removed.id)
} else {
None
}
})
.collect();
return Some((tx_data, removed_intent_ids));
}
}
}
None
}

You can find the whole implementation here:

State machine and parallelized execution model

Unlike the architecture of most existing protocols, on Anoma the application of the transactions is in step-by-step fashion, but the validation of the transactions is done after the fact, and not interleaved within the execution. In other words, the validation of state transitions is decoupled from the step-by-step execution making it stateless, which allows for easy parallelization. Assurance and correctness of state transitions is facilitated by the deployment of validity predicates (VPs).

Validity predicates

You could think of VPs as invariants on user accounts. Traditionally, the concept of invariants has been implemented in the core protocol itself, e.g. upper bounds in the total supply of a cryptocurrency. On Anoma every user account comes with a basic VP covering the security constraints, for instance that only signature of the corresponding private key is allowed to authorize spends, that the account has enough balance to spend a certain amount of funds, that a certain key can update the VP, etc. This enables two very interesting things:

One comes from the fact that the state machine will only enable the state transition to pass (and be subsequently recorded on the ledger) if none of the actions and interactions involved in all transactions in aggregate in a block conflict with other or violate the rules of any VP of any accounts that are affected by the changes.

The second interesting property is that validity predicates can be anything: they can be a simple set of logical constraints, to a function, or even an entire algorithm. Most importantly, validity predicates verify state changes, and they do not need to perform the compute of the latter, thus, users can involve as many NP-hard state changes as they want.

  • For illustration, you could configure the validity predicate of your account similarly to how you would configure the security features of an online bank account e.g. limiting transfers over a certain value, pre-authorizing transfers every month to a specified recipient, etc.
  • Beyond financial and economic appliances, users could leverage this to express preferences with higher dimensionality, such as dictating that the interactions of this account can only be performed when the cost of carbon is incorporated or that only interactions in zero-knowledge privacy are allowed.

Here’s a basic user account validity predicate implemented in Rust:

[...]#[validity_predicate]
fn validate_tx(
tx_data: Vec<u8>,
addr: Address,
keys_changed: HashSet<storage::Key>,
verifiers: HashSet<Address>,
) -> bool {
debug_log!(
"vp_user called with user addr: {}, key_changed: {:?}, verifiers: {:?}",
addr,
keys_changed,
verifiers
);
let signed_tx_data =
Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..]));
let valid_sig = Lazy::new(|| match &*signed_tx_data {
Ok(signed_tx_data) => {
let pk = key::get(&addr);
match pk {
Some(pk) => verify_tx_signature(&pk, &signed_tx_data.sig),
None => false,
}
}
_ => false,
});
[...]

Interestingly enough, you could look at validity predicates as persistent expressions of preferences (although they can be updated at any time), whereas intents are ephemeral expressions of preferences.

For example, if a user Alice wants to only make interactions that incorporate the cost of carbon, Alice could customize the VP of her account in a way that only transactions that fulfill this requirement are valid. However, in the case Alice just wanted to buy a specific good that incorporated the cost of carbon once, then she could craft an intent that sets the characteristic.

Special validity predicates

The architectural implications of making the verification and compute split more explicit go beyond efficiency and enhanced user experience. In fact the concept of validity predicates makes it possible to separate as much logic from the core architecture of the protocol as possible, to an extent that as many features and properties are implemented as VPs. These VPs have special access rights to protocol invariants.

Examples of special VPs that implement protocol-wide features are the IBC integration, PoS, governance, tokens (e.g. the native token), or privacy features such as multi-asset privacy via the multi-asset shielded pool (MASP) as a VP.

Here’s an example of a simple governance mechanism VP in Rust:

[...]pub fn proposal_vp(tx_data: Vec<u8>, addr: Address, keys_changed: HashSet<Key>, verifiers: HashSet<Address>) {
for key in keys_changed {
if is_vote_key(key) {
return (is_delegator(verifiers) || (is_validator(verifiers) && current_epoch_is_2_3(addr, id))) && is_valid_signature(tx_data);
} else if is_content_key(key) {
let post_content = read_post(key)
return !has_pre(key) && post_content.len() < MAX_PROPOSAL_CONTENT_SIZE;
} else if is_author_key(key) {
return !has_pre(key)
} else if is_proposa_code_key(key) {
let proposal_code_size = get_proposal_code_size();
return !has_pre(key) && proposal_code_size < MAX_PROPOSAL_CODE_SIZE;
} else if is_grace_epoch_key(key) {
let endEpoch = read_post_end_epoch(addr, id, END_EPOCH_KEY)
let graceEpoch = read_post_grace_epoch(addr, id, GRACE_EPOCH_KEY)
return !has_pre(key) && graceEpoch > endEpoch;
} else if is_balance_key(key) {
let pre_funds = read_funds_pre(key)
let post_funds = read_funds_post(key)
let minFunds = read_min_funds_parameter()
return post_funds - pre_funds >= MIN_PROPOSAL_FUND;
} else if is_start_or_end_epoch_key(key) {
let id = get_id_from_epoch_key(key);
let currentEpoch = read_current_epoch();
let minPeriod = read_min_period_parameter();
let startEpoch = read_post_start_epoch(addr, id, START_EPOCH_KEY);
let endEpoch = read_post_end_epoch(addr, id, END_EPOCH_KEY)
return !has_pre(key) && startEpoch - currentEpoch >= MIN_PROPOSAL_PERIOD && (startEpoch - currentEpoch) % MIN_PROPOSAL_PERIOD == 0;
} else if is_param_key(key) {
let proposal_id = get_proposal_id();
return is_tally_positive(proposal_id);
}
} else {
return false;
}
}
}
[...]

You can find the complete version of the implementation here:

Proof of Stake and Consensus

The sybil resistance mechanism on Anoma is Proof of Stake with cubic slashing with the purpose of discouraging validators that operate multiple validating nodes with replicated security configurations. Unlike linear slashing where validators get slashed a fixed percentage, with cubic slashing the percentage is at minimum 1%, but it becomes 100% if a third or more of the voting power commit faults during close time periods. Below a prototype of cubic slashing:

function calculateSlashRate(slashes) {
votingPowerFraction = 0
for slash in slashes:
votingPowerFraction += slash.validator.votingPowerFraction
return max(0.01, min(1, votingPowerFraction**2 * 9))
// minimum slash rate is 1%
// then exponential between 0 & 1/3 voting power
}

In early versions, the staking rewards mechanism follows an improved version of the F1 Fee Distribution design to minimize the amount of interactions involved in staking, withdrawing rewards, and restaking rewards. Instead, the rewards will auto-compound, while users will still be able to change specific parameters such as the validator.

Initial versions of the protocol deploy Tendermint, a form of Byzantine Fault Tolerant (BFT) consensus mechanism. Later versions deploy more advanced forms of interchain consensus, in particular Heterogeneous Paxos. The ability to share consensus across the states of heterogeneous chains is particularly interesting due to Anoma’s way of scaling via fractal instances.

Fractal Scaling

In a more mature stage of the Anoma ecosystem, there are multiple sovereign chains (fractal instances) that provide different features (e.g. one provides multi-asset privacy, another works as a settlement layer, another works as a bartering protocol, etc), have different characteristics in the protocol (PoW, PoA, PoS, or any other forms of sybil resistance mechanism), or are localized versions (e.g. a settlement layer operated by validators in Germany, another by validators based in Berlin, and another operated by folks based in Kreuzberg).

The graphic below portrays Anoma’s worldview, characterized by the co-existence of many forms of the Anoma protocol (that focus on different features, represented by the different geometrical shapes) and many kindred instances (fractal instances) of arbitrary sizes that are operated by different collectives:

Anoma’s worldview composed by many forms of the protocol (different shapes) & many fractal instances of the same form co-existing

Fractal instances of the same form might connect to each other (coloured arrows) and different forms as well (dark gray arrows below):

In Anoma’s worldview the many forms and fractal instances might be interconnected to kindred and/or different instances

The diagram below portrays the co-existence of multiple fractal instances of Anoma that focus on enabling a settlement layer for transactions:

A form of Anoma that works as a settlement layer starting from a global instance and connected to some local instances

This form of scaling is the one that enables the incentive overlap between builders, operators, and end users the most and as much architectural flexibility among the instances as possible. For example, the operators of the instances of Berlin, Germany, and EU could decide to interoperate with each other due to the organic economic interdependence, whereas the instance of the Kreuzberg neighbourhood could decide to only interoperate with the global instance and with specific instances such as Full Node’s (this is one of our offices, come visit our team in Berlin!).

Beyond interoperability, the fractal instances could deploy different governance structures, e.g. if Full Node is a cooperative, the reward distribution mechanism in PoS and fee distribution from the transaction settlement could be distributed equally across all the members of the cooperative, or they could decide to allocate a portion of the latter to a treasury dedicated to further local public goods, such as the Görlitzer Park.

Privacy

Anoma is designed in a way so that all user interactions with features from the peer-to-peer layer to the state machine can be performed while preserving full zero-knowledge privacy — subject to the preferences of the user.

An early fractal instance of Anoma that focuses on enabling asset-agnostic shielded transfers leverages the MASP circuit so that all kinds of assets, fungible and non-fungible ones, and from native to non-native assets (transferred over a custom bridge or IBC integration) can share the same anonymity set. As anonymity sets are a collective good, where the more usage, the stronger the privacy guarantees for everyone independent of their preferences (some might not care about privacy), this fractal instance of Anoma deploys a scheme to incentivize the shielded pool’s usage. To enable both multi-asset shielded transfers and incentives for the shielded pool’s usage, a custom circuit is required (see burn and mint).

In future fractal instances of Anoma that aim to enable more complex forms of coordination, such as n party trades or barters, there will often be the requirement for certain data to be transparent so computation can be performed. For instance, in the three-party barter example above, certain information such as the what ($potato, $veal, $apple) must be transparent, while the who can remain private. With the goal of supporting privacy-preserving bartering, as well as future coordination use cases, Anoma’s architecture accounts for the support of Taiga, a generalised private state transition framework with support for user and application state, which is integrated with the intent gossip and matchmakers layers.

Privacy-preserving developer tools

In alignment with the vision of Anoma to enable self-sovereign coordination, the Anoma ecosystem encompasses platform agnostic developer tooling for privacy-preserving applications, such as Plonkup, Vamp-IR, and Juvix.

Plonkup is the most configurable proof system that allows developers to write any kind of SNARKs: without trusted setup, recursion, accumulation schemes, etc; the implementation is done in collaboration with other ZK-Garage members).

Vamp-ir is a proof system-agnostic intermediate representation that any programming language can target to (including Juvix). At the same time, Vamp-IR can compile to any circuit backend including R1CS, vanilla Plonk, Plonk with lookups, Plonk with custom gates, or any other constraint system.

Juvix is a dependently typed programming language for writing backend- and proof-system-agnostic zero-knowledge applications including validity predicates on Anoma.

Final remarks

You made it until the end? Woah, thanks for reading! I hope this article was not only able to answer your questions on how Anoma works, but also that it helped you gain the intuition of its use cases and unique applications that it can enable. Now that we have an overview of the architecture, I hope to be able to inspire you with more writings that describe some of the most interesting use-cases that Anoma’s peculiar architecture can enable.

Resources

For further explorations of each component:

  • Anoma’s specifications: They are open-source and you might’ve noticed that they’re often referenced. Note that the specs are constantly being updated in content and structure, don’t be discouraged if they’re not perfect yet and please revisit it from time to time.
  • Anoma’s GitHub Org.: You will find here the codebases of Anoma, Juvix, MASP, Ferveo, the specs, and upcoming components. We’d really appreciate the stars if you enjoy any repository!
  • Anoma papers and research papers: You will find the whitepaper, vision paper, Bandersnatch, and PlonkUp cryptography papers. Whenever there are other publications they’re under this link.
  • Anoma’s blog: Notable series are Anoma Basics (new!), Anoma Tangram (explainers of each component), Agents of Anoma (about the roles in the network), and in-depth technical articles on cryptography, security, and consensus.
  • Anoma’s Medium page: For updates, such as the monthly R&D updates or the year-in-review.

--

--