An opinion piece from Josh Primero, Head Protocol Architect at RDX Works.
In my previous post, I went over 4 flaws in Sui’s MoveVM:
1. Object Primitive Which Is Too Generic
2. No Dynamic Dispatch
3. Awkward Mailbox Pattern
4. Awkward Shared State Programming
In a nutshell, Sui’s MoveVM makes several compromises to the user and programmer experience in exchange for the ability to more easily parallelize transaction execution. In this post, I will go over how Radix Engine, the engine powering the Babylon network, avoids the flaws found in Sui’s MoveVM while retaining the benefits.
1) Object Primitive Which Is Too Generic
In my previous post, I described how a Sui developer is burdened with defining and managing the lifetime state machine of new types and objects they’ve created. Managing such a low level abstraction gets in the way of what DeFi developers really care about: logic around assets.
With Radix Engine, we doubled down on the idea of explicit separation of business logic and assets. Our “big bet” was that this was the missing abstraction of DeFi and our entire design was built around this idea. So rather than a single generic object abstraction, Radix Engine instead exposes two primitive metatypes to the application layer: Components (stateful business logic) and Resources (assets).
Similar to Sui’s objects, components and resource objects also have lifetime state machines. The difference is that these are predefined by the system and can be understood and programmed at a higher level of abstraction.
From these state machines, we can derive concrete rules that we can rely on:
- Components after creation must either be globalized (the Radix Engine version of Sui’s “share”) or owned by another component as a child
- Once part of the state tree, a component’s location is locked and may no longer move or be deleted
- Resources can be moved through bucket objects and stored in vault objects
- Resources may be burned
Although at first glance these constraints may seem less “flexible”, they allow for higher order business logic complexity to be expressed more concisely without getting bogged down by low level abstractions.
Let’s go over some of the benefits provided by this model.
Wallets and Web3 Dapps are easier to implement
Because assets are system controlled, wallets and dApps can rely on a standard interface and guaranteed behavior when handling assets. This makes wallets and dApps easier to implement and more powerful in their ability to surface information to their users.
The state tree is easier to comprehend
Stateful logic forms an immutable component tree in which assets are free to flow through. This is a more intuitive model compared to a fully dynamic object state tree as is seen in Sui. Furthermore, because this semi-static structure is system defined, it allows for various optimizations in consensus implementations and off-ledger systems.
Smart contract coding is easier
Because the framework for assets and logic is system defined (rather than application defined), the language layer can bake these concepts in directly, resulting in a more DeFi-directed coding environment.
Let’s take a look at a simple product sale Blueprint (our term for Component logic) written in Scrypto, the language designed to run on top of Radix Engine.
In this blueprint, we define a way to sell a product to users at a fixed price. Calling the new function instantiates and globalizes a new component with two vaults. The buy method on this component then allows a user to buy the product by exchanging a bucket of the currency for some amount of the product at the fixed price.
Notice there is no semantic difference with plain old Rust and no additional low level semantics. Because components are defined by the system, we do not have to describe low level properties of the ProductSale type. And because resources are defined by the system, we also have a simple asset programming interface and are guaranteed that this blueprint supports all resource assets on ledger—past, present, and future.
Important Note on Scrypto
Scrypto in one sense is “just Rust compiled to WebAssembly” and, as a consequence, inherits the vast array of tooling and libraries available for Rust and WASM, something which cannot be said of new VMs and new languages like Sui’s MoveVM and Move-Language.
It is important to note though that Scrypto + Radix Engine is much more than the standard WASM + storage access + message passing architectures seen in other blockchain VMs like Polkadot, CosmWasm, NEAR etc. Such architectures cannot enforce move/borrow semantics nor the Component/Resource model just described.
What makes Scrypto different is its use of a more sophisticated Radix Engine API which enforces these behaviors. I will get into greater detail about how these APIs look in the next article.
2) No Dynamic Dispatch
In my previous post, I went over how the lack of dynamic dispatch makes Sui’s MoveVM difficult to program. Composition through interfaces, the use of stored function pointers, and the ability to update/freeze parts of code are highly used patterns in DeFi that are hard or impossible to implement with just a static dispatcher.
In Radix Engine, all methods are dynamically dispatched. This makes all of the above patterns available. For example, the following code shows how to write the incentivized future execution generator I described in the previous article:
Unlike other dynamically dispatched VMs such as the EVM, Radix Engine does not suffer from reentrancy attacks as state must be locked before reading/writing to it (this locking is managed by Scrypto, removing the burden from the developer). For example, a runtime error would occur in the above code if object_call() ended up calling itself. This is similar to Rust’s RC<RefCell> + borrow_mut pattern except that the lock is enforced by the system rather than the language.
Code Upgradability
SUI’s MoveVM takes a package-centric approach to versioning, allowing developers to iterate on code with new versions. Since package objects are immutable, this approach also allows Sui’s VM to avoid the dependency on the code state itself, allowing for better parallelizability.
There are a few issues with this approach though:
- A sensible per-object versioning scheme has to be implemented in the application layer
- The burden of using the correct package version is pushed to the package caller
- Seamless upgrades are not possible
Radix Engine, on the other hand, takes an object-centric approach to versioning. Similar to Sui’s MoveVM, the type + version represents some immutable logic. But rather than pushing responsibility of object versioning to the upper layers, Radix Engine encodes a version in the type information of every object. This allows for the possibility of seamless/opt-in/opt-out upgradability options. Function pointer tables also allow for code to be frozen or updated at a function granularity (similar to the diamond pattern in Ethereum).
Because object versioning is system managed, the degree to which an object or package is logic-frozen can always be derived without having to interpret application code. This allows users to make well-informed decisions on the risk tolerance they are willing to accept when interacting with some component.
3) Awkward Mailbox Pattern
The transfer-to-object pattern of Sui is a powerful pattern that increases parallelizability by removing the dependency on the receiving object. Unfortunately, the interface provided by Sui’s MoveVM to access this pattern is awkward and susceptible to the permanent loss of funds. For example, a user might transfer an object to another object that doesn’t support the sent object, losing access to the child object forever. This is not dissimilar to the problem of sending ERC-20 tokens to contracts that don’t support that token and losing access to those tokens forever.
The problem isn’t with the mechanism itself but with the abstraction provided to the application layer. It’s seemingly unbounded with no protections from misuse and doesn’t follow traditional programming patterns, making it insecure to use. Is there a sane way of exposing such a mechanism?
In Radix Engine, we hide this pattern below the Resource abstraction. Because resource buckets are the only “freely moving” object in the system, it’s the only object that needs to take advantage of this pattern. And because resources are system defined, this pattern can be used without ever exposing its abstraction to the programmer.
Specifically, rather than a transfer_to_object function, in Radix Engine we implement this pattern in the put method of a vault which accepts a bucket.
The eventual “receiving” of the bucket into the vault could then be committed asynchronously at a later point in time CRDT style (G-Counter for fungible resources, G-Set for non-fungible resources) or by a future user transaction as is done in Sui’s MoveVM.
Note that such a performance gain is not yet visible as the current Radix network (Babylon) does not support parallelization yet. This will become visible in Xi’an when sharding and non-contention become all important.
4) Awkward Shared State Programming
Stored References
Sui’s MoveVM removed the notion of stored references in order to be able to statically understand the state dependencies of any transaction and use such information to more efficiently parallelize. The tradeoff is that the burden of managing reference relationships is pushed to the application layer.
Radix Engine, on the other hand, is more similar to traditional Object-Oriented VMs, like the JVM, in allowing object references in object state. Unlike the JVM though, Radix Engine
- Only allows object references to global objects (JVM has no notion of owned objects)
- Does not allow NULL pointers, avoiding the billion-dollar mistake!
Type Checking for “Instance Groups”
In my previous post, I mentioned the One-Time Witness Pattern, which allows Sui to express a group of instances as a type. A consequence of this is that a new package needs to be deployed for any new instance group, and thus, creating these groups cannot be done dynamically.
In Radix Engine, we utilize a more traditional pattern with Inner (or Nested) Classes. In this pattern, a set of instances share the same parent instance. This allows the parent instance’s address to be the identifier of this group of things. This allows the Radix Engine to create new instance groups dynamically without the need for a new type. For example, this is how we’ve implemented Vaults and their relationship to the ResourceManager.
Conclusion
In this post I talked about how Radix Engine tackles some of the problems seen in Sui’s MoveVM. When we designed Radix Engine, rather than attempt to build a generic solution that happens to work for DeFi, we directed our efforts toward solving the usability problems with the creation and manipulation of assets in decentralized logic - the core notion behind DeFi and Web3. That’s why, even though we started with similar technical ideas, Radix Engine looks quite different from Sui’s MoveVM.
Radix Engine has a more complex, heterogeneous design when compared to the simple genericism of Sui’s MoveVM as it attempts to solve more issues on behalf of the developer. This makes the architecture and implementation of Radix Engine itself quite important lest it fall into complexity hell.
In the next article, I will go over our design of Radix Engine and how we borrowed concepts from Rust and Operating Systems to build a layered architecture capable of expressing all that is required for a DeFi engine.
Articles in This Series
Article 1: Thoughts on Sui’s MoveVM
Article 2: How Radix Engine Avoids the Flaws of Sui's MoveVM
Article 3: Radix Engine: A Better Model for “Enshrinement”