Aptos VS Sui: Detailed Dev Comparison

Paul Fidika
14 min readAug 29, 2022

--

Sui and Aptos are the children born from the demise of Diem; Facebook’s failed attempt at creating a blockchain. They both inherited Diem’s opensource codebase, and were formed by former Facebook engineers; Avery Ching for Aptos, and Sam Blackshear for Sui. They both raised (or plan to raise) hundreds of millions of dollars to build their own decentralized futures.

Currently Aptos is further along in development; likely launching mainnet in October 2022, whereas Sui is a bit further behind; likely launching mainnet in January 2023.

In this series of articles I’ll be comparing these two L1 blockchains.

Overall thoughts:

Aptos is very similar to, and a significant improvement upon, existing blockchains like Solana; Solana will evolve to be more like Aptos in the near future. Sui is a bit of a wildcard though; they’re trying to be very innovative and different, but it’s unclear if these changes will workout. Despite their similar origins, Aptos and Sui are almost like complete opposites of each other.

About Me:

Hi I’m Paul Fidika! I’m developing on Sui right now. If you’re interested in working together on open-source stuff, I’d love to hear from you. Get in touch: Discord Email Telegram

Table of Contents:

Part 1: The Move Programming Language
Part 2: Consensus
Part 3: Empirical Testing

Part 1: The Move Programming Language

Aptos sticks to traditional Move (as it was built for Diem), whereas Sui modifies Move considerably. In fact, Sui has changed Move so much I don’t think it’s fair to call them the same language anymore. It’s not possible to simply copy-paste code between the two; porting code requires a complete rewrite.

To illustrate the differences, let’s create a simple module. This module (1) defines a kitty resource, (2) defines a function to create kitties, and (3) defines a function rename kitties.

Aptos:

Sui:

we break this code down below.

Global Storage:

Aptos: In Aptos, Move has its own global storage, in which resources are indexed by (1) the account that owns them and (2) by the resource’s type. For example, if a module defines a resource called ‘Kitty’, and your account address is 0x69, then you would access this resource by doing something like let kitty_ref = borrow_global<Kitty>(0x69)

💡 Rule: for a given resource type, an account can only store at most one of that resource at a time. If an account needs to hold more than one instance of a resource, such as storing multiple NFTs all belonging to the same collection, then the module author can create a container resource; essentially a vector that will hold all of the user’s NFTs, and then store that vector in global storage.

Sui: Sui completely does away with Move’s global storage, and replaces it with its own global storage.

💡 Rule: In Sui, every resource is indexed with a unique global identifier.

To enable this, every Sui object (a resource that can be directly stored in Sui’s global storage) must have an id defined at the time of its creation. For example:

this id object provides a unique object_id. Sui also associates an owner_id with every object_id, which can be either an account address, another object_id, or a ‘shared object’ (more on this later).

Note: Aptos has no concept of resources owning other resources. (This is distinct from a resource containing another resource s a field, otherwise known as “wrapping”, which IS possible.)

💡 Consequence: any Sui address can own an unlimited number of resources of the same type.

For example, for the native Sui token, a single account could own 10 different Sui token objects; one object with a value of 420, another with a value of 100, another with a value of 69, etc. These fungible resources can be combined or split up into separate objects, and Sui’s Coin module provides functions to do this.

This might be a little confusing to blockchain natives, because no other blockchain has this concept; for example, on Solana, if you own USDC, you’ll always only have one USDC account associated with your address, and that account holds your entire USDC balance. To understand this better, think of each object as a ‘stack’ of money; the stacks can be combined or split up, but the total value always stays the same.

Global Storage Operators:

Aptos: In Aptos, Move has 5 global storage operators:

  • move_to: deposits a resource into an account’s global storage. This is the only operator that requires the account’s permission; the remaining 4 do not.
  • move_from: withdraws a resource from an account’s global storage.
  • borrow_global: creates an immutable reference to an item in global storage
  • borrow_global_mut: creates a mutable reference to an item in global storage
  • exists: returns a boolean, checking to see if a resource exists at an account address.

💡 Rule: These storage operators can only be used within the module that defines the resource; this means that the module authors have complete control over how their resources are stored and retrieved from global storage. Transaction, transaction scripts, and modules can only use these operators indirectly by calling whatever public functions the module makes available for them. This also means that module authors could build functions in that allow them to modify or take away resources from users without that user’s consent (I call this “weak ownership” or “module-defined ownership”).

💡 Note: because the move_to operator requires the recipients permission, this means that we cannot airdrop resources to random people’s accounts; the account owner must sign the transaction containing the move_to instruction in order to receive it.

Sui: Sui completely removes traditional Move’s global storage. Furthermore, Sui has no global storage operators; it does have the following methods, which are similar to traditional Move, but these can only be used in test packages, not actual production code:

  • take_owned<T>: takes resource type T from the sender’s global storage. Fails if there is more or less than one resource of type T.
  • can_take_owned: boolean for whether or not the above function will abort.
  • take_last_created_owned<T>: same as take_owned, but it will not abort if there is more than one resource of type T.
  • take_owned_by_id<T>: same as take_owned, except that that you must specify the object ID you want to borrow.

…and many more. Read the full list here.

This has profound consequences to Sui’s programming model, for the good and bad (we’ll explore both below).

On the other hand, Sui does have 7 ownership operators, which are used to change the owner of a resource. They’re contained inside Sui’s transfer module:

  • transfer: gives ownership of an object to a specified account address. Does not require the receiving account’s permission.
  • transfer_to_object: gives ownership of an object to another object, returning a ChildRef.
  • transfer_to_object_id: same as transfer_to_object, except you pass in an object_id rather than an object. Useful in case the object hasn’t been created yet, but you’ll be creating an object with that object_id later on.
  • transfer_child_to_object: transfers a child object from an old parent object to a new parent object.
  • transfer_child_to_address: transfer a child object from an old parent to an address (meaning it is no longer a child object).
  • freeze_object: irreversibly make an object immutable (no one owns it)
  • share_object: irreversibly make an object shared, meaning everyone owns it and can use it in any of their transactions.

💡 Rule: ownership of objects can be transferred arbitrarily by the owner, without the module needing to define its own ‘transfer’ function. This means that the module authors can never take away owned resources, or modify owned resources after the resource is initially created and given to an account; the owner must opt-in by signing a transaction. I call this ‘strong ownership’ or ‘protocol-defined ownership’.

💡 Consequence: because the transfer operator does not require the recipients permission, this means that permissionless airdrops are possible; I can send tokens to people without their consent or without them doing anything. This may be convenient as a “growth hack” for tokens looking to get quick distribution, but this will also lead to lots of spam and scams (i.e., airdropping lots of unwanted tokens to users), which is already a minor problem on Solana.

Function Arguments

Capability management is a question of ‘how does the Move VM decide what account has the rights to use a resource and call a function?’ For example, if I “own” an NFT, I should have to sign a message for it to be transferred.

Aptos: module authors in Aptos can restrict who can use a function by requiring a ‘signer’ object. The signer object is a Move native type which is produced by the Move VM. For example:

In this function, only the owner of the account can change their own kitty’s name. For example, if my address is 0x69, then only I can call this function with the address of 0x69, because only I can produce a signer object for the 0x69 account. However, if the module authors wanted to allow anyone to rename anyone else’s kitty, they would simply change the function’s signature from account: &signer to addr: address instead. That is to say, rather than requiring a signature for 0x69 as a parameter, this function would merely require 0x69 as an address, which anyone can supply.

Sui: Sui does away with Move’s native ‘signer’ type. Instead it manages capabilities by requiring senders to pass objects into its functions. For example:

💡 Rule: the Sui Move VM restricts anyone from using an object as an argument, except for its owner.

In this function, a Kitty object must be passed in as an argument. The transaction signer is restricted to only using the objects they own as arguments.

💡 Definition: this is where ‘shared objects’ come into Sui; they are objects which are owned by everyone, which means any can include them in a transaction. This is useful for markets, like DeFi protocols. In Sui terminology, ‘owned objects’ are owned by a single account, while ‘shared objects’ are owned by everyone.

Objects can be passed into functions as mutable / immutable references or by value (the function takes ownership of the object).

Standard Data Types:

Both Sui and Aptos expand on the native Move types with their own libraries, defining new custom data types.

Aptos: Introduces ‘Table’, which can manage millions of records. Note that Aptos Tables do not have a ‘length’ parameter on purpose, because they want the Aptos’ runtime to be able to mass-parallelize Table operations; i.e., insertion or deletion of records should not require the entire table to be write-locked; the runtime should only have to lock whatever table entries are being modified. This means that Tables cannot be enumerated over (I think?).

Sui: Sui data types are still in flux, but so far they’ve added UID; a global universal identifier for objects with the ‘key’ property. Additionally Sui is building the concept of child-objects; objects that stored independently in global storage (they are not nested / wrapped inside another struct) but are owned by other objects (the have a parent object). This sort of behavior is impossible in Aptos.

Key Management:

Aptos: supports single-key and K of N multiple-signature keys. For multi-sigs, all of the signatures must be submitted along with the transaction (on-chain aggregation). Off-chain multi-sig aggregation is coming in the future.

Additionally Aptos supports keypair rotation; you can change your signing keypair whenever you like, such as in cases when your private key may have been compromised or you want to upgrade to from a single-key to a multi-sig. To accomodate this, Aptos has an account abstraction, where a user’s account-address is usually different from the public-key used to sign transactions.

Sui: supports single-key and simulated K of N multi-sig. (To Do: clarify these details.) Sui does not support keypair rotation.

Multi-signer transactions:

Aptos: supports an arbitrary number of signers per transaction. This makes atomic swaps very easy.

Sui: only supports one signer at a time. Redoing this would mean changing the tx_context::sender(ctx) function to a vector of multiple senders instead of just one. Sui may add multiple-signer support if possible, although this would cause problems with FastPay in that a malicious co-signer could ‘freeze’ your objects.

Example: person-1 and person-2 both sign transaction(person_1_object, person_2_object) , then at the same time person-2 goes behind person-1’s back and also submits other_ransaction(person_2_object) . These are both valid transactions; let’s say they both accumulate 49% of the validator’s stake-weighted votes. That’s fine… BUT they are conflicting transactions in that sense that person_2_object is being used in two places at the same time. As a result, the original transaction will never be able to reach the 66% quorum majority it needs to be finalized (because the other validators already voted on a conflicting transaction), meaning person_1_object will be stuck in unconfirmed limbo until the current epoch ends (about 1 day max).

Transaction Scripts:

Move introduces an amazing new concept; transaction scripts. These allow transaction-senders to write ephemeral, single-use modules, rather than just calling entry functions on modules. This allows developers to move much of their logic out of on-chain modules and into custom-built transactions, if they wish.

Aptos fully supports these, while Sui does not yet support them. Sui technically allows you to do transaction-scripts by deploying custom modules and then calling them, however this is a two-step process, and is more expensive because you’re wasting storage space storing a module that will only be called once. I think Sui should keep transaction scripts.

Sui does support ‘transaction bundles’, which are lists of simple transactions sequenced together as an atomic unit. I believe they all must be entry functions, meaning the output of an earlier transaction cannot be used as input in a later one.

Off-Chain Data Exploration

Aptos has an indexer inside of its aptos-core repo, and Topaz is working on its own custom indexer specifically for NFT markets.

Sui does not yet have an indexer, although one is in the works.

In Aptos and Sui, move-modules can emit events, which are droppable, copyable structs. These events are not used on-chain, and are used solely as an index for querying RPC nodes.

Misc. Differences:

Aptos: uses 32-byte addresses (the default Diem address size), which are encoded as 64-digit hexadecimals, looking like this:
0x375153e0983320667c8ccd98fd0b7057b35bc8a82670d10a6b44506f189bd4f1

Sui: went with 20-byte addresses instead, which are encoded as 40-digit hexadecimals, looking like this:
0x3c17c86120d41b854f2603d001eba3705eb16368

Thoughts for Improvements to Aptos

Gas schedule: currently, the gas schedule does not give you any reward for parallelism, which is a very important part of Aptos (block STM). A better gas-cost system would reward developers for using data-structures on which transactions can be run in parallel, and avoiding data-structures that force transactions to be done sequentially (example: if you create a table that has a ‘length’ property which needs to be updated on every insertion and deletion).

Additionally, developers should be incentivized to de-allocate resources they are no longer using, i.e., if you delete a struct from global storage, you should receive some storage refund.

Summary Comparison:

Airdrops:

Permissionless airdrops are not possible in Aptos; the recipients must opt-in to receive resources from you prior. For Sui, mass permissionless airdrops are one of the main use-cases Sui was designed for. In my opinion, being able to drop random objects into any account’s wallet is more of a bug than a feature, so I think Aptos has the better idea here.

On-Chain Exploration of Objects / No Global Storage Operators:

Because Sui doesn’t have global storage operators, there is no ‘on chain exploration’. In our first example code, our Aptos module can check global storage to see if our Kitty resource exists already when we try to rename it, and if it doesn’t, then we’ll create the Kitty resource and store it prior to renaming it. On Sui, this sort of on-chain check is not possible.

Sui expects us to give object-ids for our function arguments, but how are we supposed to know what object-ids we can use? This introduces a lot of off-chain logic, which must be placed inside of the wallet or the dapp; in order to construct a transaction, the wallet or dapp must query a Fullnode, get a list of objects the would-be-signer owns, select an object from the would-be-signer’s account, and then present a transaction with those object-ids filled in. This is a lot of work, and I don’t like the logic being all off-chain.

I would like to say though that Sui is very ‘cute’; I like to visualize Sui storing a bunch of ‘spheres’ that represent resources, and then every function being a ‘machine’ with slots waiting for spheres to be inserted. You grab the right spheres, insert them into all the slots to active the machine, and then WHIRRRR out comes some newly processed spheres at the bottom. It’s a very intuitive system.

One advantage of Sui for validators is sharding their memory; a validator only needs to know about the objects being used in the current transaction, all of which are owned by the same person; they don’t need to worry about storing the ENTIRE state of all accounts on the blockchain that could potentially be accessed by a global-storage call. This makes Sui functions pure functions, because they do not rely upon or modify global storage.

Solana’s current biggest bottleneck is disc read-write speeds; memory access, rather than network traffic or CPU usage will likely be the largest bottleneck in most blockchains, so this is a smart move on behalf of Sui.

Even so, I would like to see object-selection and object-conditional logic moved on chain; I imagine that transaction-scripts could consist of two phases; (1) logic to select objects from global storage that will be used in function calls, and (2) the actual function calls.

Code Safety:

The signer object in traditional Move (Aptos) is very powerful; essentially if a malicious module can get ahold of your signer object (you signed its malicious transaction), the module can take that object and do whatever it wants with it, including withdrawing arbitrary tokens and transferring it to another address. This is true in most blockchains however; you have to be careful what you sign! Which is why transaction simulations are important.

In Sui however, a malicious transaction can only ever steal or harm whatever objects you passed into the function call. This provides somewhat more protection, because it’s much easier to predict what resources will be affected by a transaction.

Code Complexity:

Overall, Sui will be a considerably more complex chain to build on, at least in its current incarnation. The complexity mostly stems from managing objects (see above), and wallets being much more engaged in validating a transaction (see Part 2).

Weak Ownership VS Strong Ownership:
(Module Defined Ownership VS Protocol-Defined Ownership)

In Aptos, modules have complete control over defining how their resources are controlled, the same as in Ethereum. I call this ‘weak ownership’; it is defined at the module-layer.

In Sui, ownership is a protocol-level primitive, and users can transfer ownership regardless of what the module authors want. This is a very powerful, key distinction. Let me give some examples:

Suppose that Circle creates a module that defines USDC as a resource; and Circle wants to be able to give themselves the ability to freeze an account’s USDC from being transferred whenever they get a request form law-enforcement to do so; they cannot do this.

Suppose an NFT project on Sui messes up half its mint, giving half of the buyers malformed metadata. The project wants to be able to correct this bugged-out metadata; they cannot do this without the owners signing a migration transaction to do so.

Module-authors could get around this by making all of their resources shared-objects, rather than owned-objects, and then building their own ownership-checking systems within each object; i.e., writing down the address of the resource’s ‘owner’ as a field inside the struct, then checking that address versus the transaction sender’s address for every transaction, but this is a terrible, hackish solution that ruins many of the advantages Sui is introducing.

The primitive that Sui is missing the most is capability delegation; perhaps you ‘own’ a resource in Sui, but you should be able to grant limited control over that resource to other addresses without turning owned-objects into shared-objects. In the above example, Circle would very much like to be able to call some confiscate_asset function that removes your ownership of their USDC, preventing you from being able to transfer it. Also the NFT project would like to be able to call some update_metadata function on each malformed resource they created. These are both legitimate and common use-cases on other blockchains. I think this capability delegation needs to be built into Sui at the protocol-level. Essentially this would allow other accounts to use objects they do not own, but in a limited, permissioned way.

Additionally, I’d like to point out that ‘transfer’ being a protocol-wide definition, rather than a module-wide definition, along with the Bag datatype, makes building marketplaces very simple; on Aptos every module can define its own custom transfer function, meaning marketplaces need to build separate functions for each type of resource they deal with, while on Sui you can create generic marketplaces that work for any arbitrary resource. I like this level of genericness, even though it takes some control away from module authors.

Part 2: Consensus

(Coming eventually…)

Part 3: Latency and Throughput

(Coming at some point — let’s try to break some testnets!)

--

--