Thoughts on intents in GOOSE
This post describes my attempts at making various approaches to encoding intents in GOOSE more concrete, and the issues encountered / envisioned. These are only sketches of possible approaches, not detailed implementations. They summarize my current understanding and the foreseen issues with each approach.
Intents as unbalanced transactions
This is the “canonical” implementation of intents as described in Anoma docs. In GOOSE, a version of this approach was present in v0.2.x (see: intent summary and intent translation summary). The intents were implemented by intent bearing resources whose RLs checked the intent conditions.
The problem with this approach is that, unless intents are statically known beforehand, it is possible to use them to circumvent the official object interface.
A somewhat contrived but illustrative example is as follows. Suppose apples can be green or red. The color of an apple is encoded in the value field of the corresponding resource, so its preservation is not ensured by the RM balance check. Suppose that the Apple object interface doesn’t allow apples to change colors (or allows it only under certain conditions - details don’t matter for the purposes of the example). Using unrestricted intents, malicious users can violate this invariant.
User A creates a partial transaction with consumed: 1 Apple green of A, created: 1 Banana of A. User B creates a partial transaction with consumed: 1 Banana of B, created: 1 Apple red of B. When the intents are matched and aggregated in the final transaction, we end up with a transfer of a green Apple from A to B which changes its color to red. The transaction balances because the colors are not included in the balance check.
Without restricting the intents allowed, any invariant on object’s fields can be violated in similar manner. The problem is that the balance check does not (and in general cannot) check the preservation of the value field in resources.
To solve this problem, RLs of object resources check if the object resource is consumed to create one of statically known intents. This makes it impossible to dynamically add new intents, i.e., without modifying the kinds of object resources involved.
Intents as incomplete programs
One idea is to represent intents as incomplete programs that are matched together to create a complete program. The complete program performs the necessary transfers.
Below is an attempt at making this more concrete, where A and B want to exchange provided objects under some conditions on objects obtained. The “program” is identified with a logic expressing the conditions.
Intent message resource contains:
logicRef
signatureA
signatureB
RL of the intent resource checks:
- consumed objects = provided A + provided B
- created objects = obtained A + obtained B
signatureAverifiesmsg.logicRefsigned by AsignatureBverifiesmsg.logicRefsigned by B- check intent logic A (conditions on obtained A)
- check intent logic B (conditions on obtained B)
Steps:
- A sends its intent logic A (conditions on obtained A) and public key of A to the Solver.
- B sends its intent logic B (conditions on obtained B) and public key of B to the Solver.
- Solver creates combined intent logic L, sends it back to A and B.
- A inspects L, compiles it, signs ref of L with private key of A producing signature A and sends signature A and provided A to Solver.
- B inspects L, compiles it, signs ref of L with private key of B producing signature B and sends signature B and provided B to Solver.
- Solver creates intent message, submits transaction.
Problem:
- The objects involved in the transaction (consumed = provided A + provided B) need to statically know about the intent message (to allow their consumption for the intent message).
Solution:
- Intent message type has separate logic in the object resource RL. If message type =
intent, then check that either:signatureAverifiesmsg.logicRefsigned by owner ofself, orsignatureBverifiesmsg.logicRefsigned by owner ofself.
Problem:
- We have the same interface circumvention problem as we had before, because nothing prevents the participants from violating the intended interface of consumed and created objects, e.g., failing to preserve object invariants.
Simplification:
- We could split intent message into two intent resources for A and B, then no back-and-forth with the Solver transmitting logics is necessary - A and B can just sign their own intent logics separately in their intent messages.
- This solution essentially amounts to the original intent implementation (intents as unbalanced transactions), but with mandatory ownership checking for the objects involved.
Intents restricted to official object interface
This is an attempt at concretising Chris’s approach from the forum for the current Resource Machine, in the current GOOSE framework. TL;DR: I don’t know how to implement the “require” from the post (requiring another object to send a message) in a way that would be binding and ensure the required transfers actually happen without the need to trust anyone. Perhaps this needs an adjustment of the RM model, or I’m just not seeing something.
Below is a sketch of an example for the current GOOSE model.
We have two users A and B who want to exchange kudos K1 and K2 under the following conditions.
- A: Exchange 6 K1 for >= 9 K2
- B: Exchange 9 K2 for >= 6 K1
Kudos:
- Kudos bank object which keeps track of user balances
- K1, K2 - Kudo kinds / denominations
Interface of Kudos bank:
Kudos.transfer(X, Y, kind, amount)method: transferamountofkindfrom accountXtoY, check if caller authorised to transfer from accountX
Steps:
- A:
IntentA := KudosSwapIntent.create(K1, 6, K2, 9) - B:
IntentB := KudosSwapIntent.create(K2, 9, K1, 6) - A:
Kudos.transfer(A, IntentA.uid, K1, 6) - B:
Kudos.transfer(B, IntentB.uid, K2, 9) - A: submit
IntentAto solver - B: submit
IntentBto solver - Solver:
KudosSwapIntent.solve(IntentA, IntentB)
class KudosSwapIntent:
kind : Kind
amount : Nat
wantKind : Kind
wantAmount : Nat
constructor create(kind, amount, wantKind, wantAmount):
return KudosSwapIntent {..}
-- We need to ensure that only this multi-method is authorized to submit
-- a transfer from an intent object
def KudosSwapIntent.solve(intent1 : KudosSwapIntent, intent2 : KudosSwapIntent):
check (intent1.kind == intent2.wantKind)
check (intent1.amount >= intent2.wantAmount)
check (intent2.kind == intent1.wantKind)
check (intent2.amount >= intent1.wantAmount)
Kudos.transfer(intent1.uid, intent2.owner, intent1.kind, intent1.amount)
Kudos.transfer(intent2.uid, intent1.owner, intent2.kind, intent2.amount)
Problem:
- We need to ensure that only the
KudosSwapIntent.solvemulti-method is authorized to submit transfers fromintent1andintent2.
Solution:
- Trust the solver - transfer to solver instead who does the necessary transfers once the intents are matched.
- From what I know, this would be similar but more general than how DEX currently works - essentially the users trust the DEX to do the right transfers.
- I don’t know how to eliminate the trust assumption in the current RM / GOOSE framework.