The goal of this post is to describe the problem of interoperability of different paradigms and propose the short- and long- term solution that involves minimal changes but preserves maximum generality.
Declarative vs imperative
Dividing the execution flow into two parts, we can talk about high-level, specification layer and low-level, execution layer. The execution layer always operates in the imperative paradigm - a specified sequence of commands is executed. In contrast, a declarative paradigm doesn’t require specifying how to do something but only what the start and end states are. The specification layer can be described in both imperative and declarative ways. The ARM operates in declarative specification paradigm. Ethereum and most other blockchains operate in imperative specification paradigm.
Going from imperative specification to imperative execution layer is fairly straightforward: for example, if I say “buy me a red apple from the Rewe nearby”, the execution steps include getting out of the house, going to Rewe, buying an apple, going home, etc.
In the declarative paradigm, I just say “I want a red apple” and another actor figures out how to get it (to buy or to grow?) and where to get it from (Rewe nearby or go to Frankfurt?).
Interoperability scenario: local assets, external services
Let’s consider the following scenario: a user holding assets on Anoma wants to use an application on these assets that is hosted on another chain, for example, Ethereum (from now on we will use Ethereum as a universal example). That requires the following steps:
- Off-chain: construct the Anoma transaction that provides the resources and specifies the desired end state
- Anoma (declarative step): execute the Anoma transaction, bridging the assets to Ethereum
- Ethereum (imperative step[1]): Call the relevant application contracts on the provided assets
The following problem arises: the scenario initiates on the Anoma side - the side that provides the assets, but by definition, an Anoma transaction doesn’t specify how to achieve the state desired by the user. The imperative chain that hosts the application requires a sequence of commands. How do we figure it out?
Who figures out the order?
There are multiple ways. Let’s consider some:
1. The user specifies the order off-chain (during step one: off-chain)
That option is the simplest in the sense of implementation, but does take away the declarative advantage of the ARM design. Each paradigm comes with trade-offs, and if we don’t use the advantage it gives, we are only left with the downside part of the trade-off. However, this might be an acceptable solution in the aforementioned scenario where the user already intends to use an Ethereum dapp (and accepts the trade-offs of the imperative model) and only needs the Anoma part to provide the assets.
2. The solver completes the transaction with ordering information (during step one: off-chain)
This option is most fitting assumption for our (long-term) model: users specify what they want (declarative specification) and solvers figure out the path (imperative execution). However, that solution assumes a solver network (which might consist of a single solver) and solving (no pun intended) some associated safety and privacy problems. Since the solver would have to complete the transaction, the user must either trust the solver to see the transaction and modify it, but not tamper with it, which doesn’t scale, or we need to develop a mechanism that enables selective integrity - modifying the transaction in the allowed ways without modifying it in the disallowed ways. In short, it will take some time to get there.
3. A contract figures out the order (during step three: on Ethereum)
Given an Anoma transaction with multiple resources that perform external contract calls, the PA determines which calls have to be executed in which order to achieve the desired state. Generally, figuring out the order of the external calls is a non-trivial task. However, if in the generic call design we expect the same pattern (move assets - perform various external calls - move new assets back), the task becomes way simpler.
It might be unwise, however, to put this job onto the PA, since the generic call application, even though being called generic, still enables only a subset of what can be done via the protocol adapter. In other words, the protocol adapter design is more generic than the generic call resource design.
On the other hand, it might still be acceptable to do that if:
- We don’t expect to go beyond this pattern - if GC affordances is the most we expect of the PA design in the long-term.
- We don’t mind including this logic in the PA temporarily until we find other affordances that do not require this step (and then move this logic elsewhere). At the moment, the GC design represents the largest surface of what can be done via the PA.
If we want to preserve architectural generality and don’t want to tweak contracts temporarily, it might be wiser to introduce an intermediate step contract that is responsible for ordering of actions that include GC resources. That, of course, is in conflict with the long-term goal of FP since it is based on the fact that both the PA and contracts can tell the difference between GC resources, asset provisioning resources, and other kinds of resources.
What to actually do?
Having said all that, I propose to move forward with the first option for now and the second in the long-term. The first option is the simplest but still requires changes.
1. Make execution on PA deterministically ordered
The PA will have to check for the provided ordering information in the payload and execute calls in the specified order. For the transactions where order doesn’t matter, a random order should be picked to avoid ordering information leakage - the observer won’t be able to tell if the order is picked or assigned randomly.
2. Lift payload constraints in the TTC circuit
At the moment we strictly constrain all payloads in the TTC circuit to be either empty or contain information for wrap/unwrap. We will have to (partially) lift this requirement in the circuit to allow users add ordering information. It shouldn’t introduce any harmful scenarios since by definition any order should be safe in the declarative paradigm as long as it leads to the desired outcome.
The ordering information should be included in external payload since it is..external. So we just need to make sure this and any other external information that doesn’t need to be constrained could be included in the payload.
3. Avoid overconstraining in circuits
Longer-term supporting solution here would involve the ARM changes that would allow storing arbitrary not constrained information in a transaction / action. As long as this data cannot harm, it should be a fine enhancement.
In the short-term, however, we just need to make sure we don’t constrain the circuits in a way that leaves no space for harmless field usage. Given that we don’t have a lot of active circuits, that should be easy.
Specifically, we need to allow inclusion of external information that we don’t need to constrain but that could be helpful in other contexts (such as imperative ordering information that cannot harm).
4. About the order in the action tree root
It is true that we currently recompute the action tree root based on the order of the resources in the action. While it might turn out fragile in certain contexts in the future, I don’t actually see harm in that in the short-term if it is decoupled from execution. We just need to remember that the order of resources is neither deterministic nor meaningful.
So..why not encode it in resource order within the action?
After writing all this I asked myself the same question. Here is why.
User specifying the order is a hack we want to use in the short term, but in the long term specifying the state and specifying the execution order are decoupled from each other with the latter happening after the actions are “sealed” by the user. The user provides the resources and the desired end state, signs it (or authorizes another binding way) and hands it to the solver.
At that step, the execution order is not known but the order of resources in the action is fixed.
In the next step, the solver will figure out the optimal execution ordering and everything else for the user, add this information to the transaction, and send it further.
Dedicating a separate field for ordering and other solver information will provide not just a cleaner approach. Delegated ordering is simply not possible if the user has to sign the execution order before the solver figures it out, and signing has to happen before to ensure tampering resistance.
It might happen that assumptions will change in the future and the solver’s expected work will include more/less stuff. Either way, separating authorization from figuring out the execution order is architecturally cleaner: the user authorizes the state and the order can be added later.
- Anoma and Ethereum interoperate on the same level here, so both steps refer to the high-level paradigm. In the Anoma case it is obvious because high-level and low-level paradigms differ. For Ethereum, they are the same.
︎
Anoma and Ethereum interoperate on the same level, so both steps refer to the high-level paradigm. In the Anoma case it is obvious because high-level and low-level paradigms differ. For Ethereum, the paradigms are the same so it might be unclear when we are talking about low- or high-level. ↩︎