The Consequences Of Generalizing Transaction-Wide Checks

Having a single fixed transaction-wide check – balance – makes a lot of things easier. We don’t have to come up with a generalized enforcement mechanism but have to just hardcode it in the system. Now that we are considering generalizing it, let’s try to imagine how it could work and answer some questions.

First of all let’s define what has to happen in order for a transaction to be considered valid:

  • the rules that facilitate the event log structure must be verified
  • every application involved in the transaction must authorize it (kind-enforced checks)
  • the custodians of all resources that are being consumed must authorize the transaction [1]

So we have three mandatory actors (the system, applications, and resource custodians) and some optional actors (usually other users) involved by the applications.

Actor Identity across transactions Constraints across transactions
The system Fixed Fixed
Applications Variable Variable
Resource custodians/provers Variable Fixed
Optional actors Variable Prescribed by the application

Who has a say?

Allowing transaction-wide checks enables constraint variability on the transaction-wide level. Note that only applications’ constraints vary across transactions, even though the identity of all actors (except the system) may be different. Therefore applications are the only entities who may affect transaction-wide checks (the users get their variability only if the application enables it but they have agency over what application to interact with).

Depending on the branch, the same application may require different transaction-wide checks. Let’s explore how it would work in more detail.

AnomaClay

Imagine we do have transaction-wide checks that applications can influence based on the function. Let’s design a simple application – we will call it AnomaClay – that allows minting, burning, and transferring resources of the AnomaClay kind. Assuming no requirement for the balance check, we structure it as follows:

  • mint and burn do not require balancing but do require external evidence (likely provided by the user) to authorize the transaction
  • transfer check requires balance check

And here we have an immediate problem: having mint and transfer in the same transaction imposes confusing constraints on the whole transaction:

  • transfer requires a transaction-wide balance check (we don’t want to assume that both resources are in the same action, you will see why below)
  • the transaction won’t balance because of the mint included in the same transaction

You might wonder: can we just make the transfer check local - limited to the action that contains the consumed and created resource? The short answer is: not if we want to have intent matching.

Here is a more complex version of the same transaction that helps to answer this question in more detail:

The diagram above describes a transaction that includes:

  • two cross-action transfers (simplest intent-matching without IBR),
  • a mint,
  • a burn,

the last two not directly related to transfers (even though produced by the same entities in this example).

Local balance check wouldn’t enforce the required transfer conditions. We also cannot tie the checks to either consumed or created resources since neither is unique for transfer branch.

Things would work if we had a way to specify the constraints that cover just the relevant portion of the transaction, so not quite transaction-wide. I see a few ways to do that:

  • limit the transaction scope. In that case, enforcing checks becomes easy since transaction-wide = relevant, but limiting the scope comes with its downsides, namely, we can only have one function per transaction. This can be limiting in cases the user wants to perform multi-step transaction, e.g., deposit some tokens and transfer them to another person right away[2]. Limiting the transaction’s scope to a single function prevents these kinds of tricks.
  • determine each call’s scope. No more transaction-wide checks, each check has a dedicated scope within the transaction (not necessarily limited to a single action). Functionally this is similar (but orthogonal) to how actions partition the transaction based on the proving unit. To enable this, there must be at least one party that has full view over the scope partition unit in order to be able to create a proof of a partition-wide check (like the solver in the current design is assumed to have access to the balance data to be able to create delta proof)

Elephant in the design

This makes me wonder if this complexity tower is a consequence of the design choices that poorly fit the intended model. Let’s try to state some of the properties we want to have:

  • View locality - enable spectrum of viewing modes. Some data can stay local to the user, some data can be revealed to user subsets, some data can be public. As always, it isn’t about prescribing specific policies, it is about having a choice.
  • Atomic transaction composability - ability to turn single-branch execution (“Here I invoke the transfer branch of the circuit”) into multi-step programs (Can do transfer and mint and whatever else with other apps and across apps). Relevant questions: intra-transaction liquidity (can we consume freshly created resources from the same transaction?) and ordering-readiness criteria.
  • Intent matching - enable entangled multi-user atomic transactions via off-chain coordination. Without intent matching, the entangled state can be produced with multiple separate non-entangled transactions that involve trusted on-chain third parties. Intent matching allows to keep the entanglement and abstract away the coordination component - it can be off- or on-chain.

With that in mind the transaction might be restructured as follows:

  1. Single-user, single branch context. Corresponds to a logic proof associated with a single resource consumption/creation. Local.
  2. Multi-user, single branch context. Corresponds to the non-existent branch-driven partition of the transaction and enables intent matching. To give an example, consider the cross-action transfers from the diagram above. Each transfer would correspond to one partition which includes two half-actions[3]
  3. Multi-user, multi-branch context. That would be the new transaction. The key reason to have such transactions, besides the practical optimization (amortization of verification costs), is to potentially enable intra-transaction liquidity, which allows us to take even more coordination context off-chain. This covers the atomic transaction composability property.

As always, this creates more questions than answers.


  1. Usually done implicitly by computing and revealing the resource nullifier but can be a bit more complex if there is some delegation involved. The complication is usually accounted for in the application logic and is therefore a part of the application branch ↩︎

  2. This requires the ability to consume non-globally-committed resources, but who says we cannot enable that as well in our dream RM world? ↩︎

  3. Here we might have to state that actions are orthogonal to partitions, define the relationship between actions and partitions, or require that partitions only include full actions, in this case expanding the partition’s scope from a single branch ↩︎

2 Likes