Thoughts on intents in GOOSE

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
  • signatureA verifies msg.logicRef signed by A
  • signatureB verifies msg.logicRef signed by B
  • check intent logic A (conditions on obtained A)
  • check intent logic B (conditions on obtained B)

Steps:

  1. A sends its intent logic A (conditions on obtained A) and public key of A to the Solver.
  2. B sends its intent logic B (conditions on obtained B) and public key of B to the Solver.
  3. Solver creates combined intent logic L, sends it back to A and B.
  4. 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.
  5. 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.
  6. 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:
    • signatureA verifies msg.logicRef signed by owner of self, or
    • signatureB verifies msg.logicRef signed by owner of self.

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: transfer amount of kind from account X to Y, check if caller authorised to transfer from account X

Steps:

  1. A: IntentA := KudosSwapIntent.create(K1, 6, K2, 9)
  2. B: IntentB := KudosSwapIntent.create(K2, 9, K1, 6)
  3. A: Kudos.transfer(A, IntentA.uid, K1, 6)
  4. B: Kudos.transfer(B, IntentB.uid, K2, 9)
  5. A: submit IntentA to solver
  6. B: submit IntentB to solver
  7. 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.solve multi-method is authorized to submit transfers from intent1 and intent2.

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.
1 Like

Thank you for the write-up!

I was roughly envisioning that require could be implemented by creating an ephemeral resource (which must be consumed elsewhere in order for the transaction to balance) which carries the constraint – i.e., checks that the requisite message is actually sent to the object specified elsewhere in the transaction, and checks that it is the only require constraint thus satisfied (to ensure linearity, which we want here). Could that work?

I think my example is somewhat different than using a multi-method, namely, the require checks would happen in the individual (ephemeral) intent objects, and no specific multi-method should be required (or need to be specially authorized) – we would need to implement this require feature in compilation, though.

1 Like

I think this could be made to work, but one needs to be careful.

We need a conditionalTransfer(A, B, denom, amount) message whose RL would check the following.

  1. Transfer logic check for a transfer of amount of denom from A to B.
  2. The conditionalTransfer(A, B, denom, amount) message was signed by A.
  3. There exists a consumed intent resource IntentA which is signed by A.
    The last point is crucial. Without it, or if IntentA wasn’t signed by A, the intent could not be enforced (an attacker could simply omit it).

IntentA is an intent resource whose RL verifies that there exists a consumed message resource signed by B which performs the appropriate transfer.

The whole exchange would look as follows.

  1. A sends its intent (constraints on provided and received objects) to the Solver.
  2. B sends its intent (constraints on provided and received objects) to the Solver.
  3. Solver matches the intents and informs users about conterparties:
    • sends id of B to A,
    • sends id of A to B.
  4. A creates a signed message resource MA for conditionalTransfer(A, B, denom1, amount1) and a signed intent resource IntentA.
  5. B creates a signed message resource MB for conditionalTransfer(B, A, denom2, amount2) and a signed intent resource IntentB.
  6. A sends MA and IntentA to Solver.
  7. B sends MB and IntentB to Solver.
  8. Solver submits a transaction with MA, MB, IntentA, IntentB and the bank object resource. In this step the Solver only aggregates the messages into a single transaction - this could be done by any single participant instead (A, B or a third party).

So we need a “conditional” version of every method we want to use in an intent. Logically, one could say that normal methods are conditional with the trivial (always true) condition.

In this way:

  • conditionalTransfer will succeed only if there exists an intent resource signed by the user doing the transfer. So it’s not possible to use the signed conditionalTransfer message to perform the transfer without the intent being satisfied.
  • The intent logic is independent of conditionalTransfer logic, so we have no restriction on the kinds of intents that can be expressed (only that they must use the object interface - can check if some messages are sent).

Potential problems:

  • The intents need to be invalidated once the exchange transaction is processed, but I guess RM handles that with nonces. The nonce needs to be included in the signature of the intent resource so that it cannot be changed (and the intent resource re-used for another exchange).
  • Theoretically, a malicious party which is supposed to create the final transaction could just not submit it, say it failed, save the intent resource and then use it later on when the same user requests another transaction to be submitted (i.e. using the old intent instead of the new one).
1 Like

A small simplification of the above is to include a cryptographic hash of the intent resource in the conditional transfer message instead of signing the intent resource. This is better because then a given conditionalTransfer is associated with a specific intent, not with any authorized intent, so there is no potential intent swapping problem. The intent hash, being part of the message, is signed by the user as part of signing the transfer message, so there is no need to sign the intent resource separately.

A simplified version of intents using cryptographic hashes of intent resources instead of signing intent resources is as follows.

The RL of the conditionalTransfer(A, B, denom, amount, IntentAHash) message checks the following.

  1. Transfer logic check for a transfer of amount of denom from A to B.
  2. The conditionalTransfer(A, B, denom, amount, IntentAHash) message was signed by A.
  3. There exists a consumed intent resource IntentA such that hash(IntentA) = IntentAHash. The hash needs to include the RL of IntentA.

The RL of the intent resource IntentA checks if B performs the desired transfer, i.e., if there exists a consumed conditionalTransfer message in which B transfers the required amount of a desired denomination to A.

Steps:

  1. A sends its intent (constraints on provided and received objects) to the Solver.
  2. B sends its intent (constraints on provided and received objects) to the Solver.
  3. Solver matches the intents and informs users about conterparties:
    • sends id of B to A,
    • sends id of A to B.
  4. A creates an intent resource IntentA and a signed message resource MA for conditionalTransfer(A, B, denom1, amount1, hash(IntentA)).
  5. B creates an intent resource IntentB and a signed message resource MB for conditionalTransfer(B, A, denom2, amount2, hash(IntentB)).
  6. A sends MA and IntentA to Assembler.
  7. B sends MB and IntentB to Assembler.
  8. Assembler submits a transaction assembled from MA, MB, IntentA, IntentB and the bank object resource.
1 Like

We definitely don’t want to require interactivity in this example (i.e. each user needing to learn the identity of the other user involved), the solver should be able to match the intents non-interactively, and the users shouldn’t need to care about the identities of their counterparties (only the assets and amounts involved). Why exactly is that necessary in your construction? Can’t the intent resource (IntentA) just check the asset and amount (but not the identity of the counterparty)?

This in and of itself is unavoidable in a certain sense – there’s nothing a user can do to force a third party to submit a message “now” – but a user can include expiration conditions in an intent (e.g. timestamp) or otherwise invalidate the intent by sending a transaction (in a slightly more complex model with some form of “intent nonce”).

I think this is a nice simplification, and indeed avoids potential problems as you mention.

2 Likes

We definitely don’t want to require interactivity in this example (i.e. each user needing to learn the identity of the other user involved), the solver should be able to match the intents non-interactively, and the users shouldn’t need to care about the identities of their counterparties (only the assets and amounts involved). Why exactly is that necessary in your construction? Can’t the intent resource (IntentA ) just check the asset and amount (but not the identity of the counterparty)?

Someone needs to create the conditionalTransfer(A, B, denom, amount, IntentAHash) message, and A needs to sign off on it. At this point, I don’t know how to do it without revealing B to A.

Why does this message need to reference B specifically?

conditionalTransfer is a message sent to the Bank object which specifies the transfer, so it needs to specify the recipient. However we formulate it, at some point we need to indicate that A wants to transfer funds to B, and A needs to authorize this.

After some thought, I believe with a bit more sophistication we could actually hide B from A. We can shift the checks of B, amount and denom to IntentA and make A prepare and sign an incomplete conditional transfer message conditionalTransfer(A, _, _, _, IntentAHash). Then the RL of conditionalTransfer would not include B, denom and amount in the signature check, but that’s fine because these can be checked by the intent IntentA. The hash IntentAHash has been signed by A so IntentA can’t be maliciously replaced. If A doesn’t care who they’re transferring funds to, they can forgoe checking B in IntentA. Then the Solver would fill in the missing recipient B, and A would never see it.

Let ProvidedX(denom, amount) be the constraints of user X on what X provides, and ObtainedX(denom, amount) be the constraints of user X on what X receives.

A version of intents with counterparty hiding is as follows.

The RL of the conditionalTransfer(A, B, denom, amount, IntentAHash) message checks the following.

  1. Transfer logic check for a transfer of amount of denom from A to B.
  2. The conditionalTransfer(A, _, _, _, IntentAHash) message was signed by A. We don’t include B, denom or amount in the signature check.
  3. There exists a consumed intent resource IntentA such that hash(IntentA) = IntentAHash.

The RL of the intent resource IntentA checks the following.

  1. There exists a consumed message resource conditionalTransfer(A, _, denom, amount, _) such that ProvidedA(denom, amount) holds.
  2. There exists a consumed message resource conditionalTransfer(_, A, denom, amount, _) such that ObtainedA(denom, amount) holds.
  3. There exists exactly one consumed message resource matching conditionalTransfer(A, _, _, _), i.e., only one conditional transfer message in which A transfers funds.

Without the last point a malicious Solver could perform an arbitrary transfer of A’s funds in addition to the transfer authorized by the intent logic. This could be done by simply duplicating the incomplete conditionalTransfer message and filling the copy with arbitrary data, while filling the original with correct data. If we didn’t check that there is only one transfer from A, the transaction would go through, because there would exist a conditional transfer message satisfying A’s intent (plus another one not satisfying it).

Steps:

  1. A sends the following to the Solver:
    • intent constraints (ProvidedA, ObtainedA),
    • intent resource IntentA,
    • signed partial message resource PMA for conditionalTransfer(A, _, _, _, hash(IntentA)).
  2. B sends the following to the Solver:
    • intent constraints (ProvidedB, ObtainedB),
    • intent resource IntentB,
    • signed partial message resource PMB for conditionalTransfer(B, _, _, _, hash(IntentB)).
  3. Solver matches the intents – it finds the counterparties A and B together with denomA, amountA, denomB, amountB such that all intent constraints hold:
    • ProvidedA(denomA, amountA),
    • ProvidedB(denomB, amountB),
    • ObtainedA(denomB, amountB),
    • ObtainedB(denomA, amountA).
  4. Solver fills in the missing fields in the conditional transfer messages PMA and PMB:
    • from PMA creates a message resource MA for conditionalTransfer(A, B, denomA, amountA, hash(IntentA)),
    • from PMB creates a message resource MB for conditionalTransfer(B, A, denomB, amountB, hash(IntentB)).
  5. Solver submits a transaction assembled from MA, MB, IntentA, IntentB and the bank object resource.
1 Like

Your latest proposal generally makes sense to me in terms of the flow (steps).

I think the only remaining difference with the model I was originally considering is that I used an ephemeral object (instead of a conditionalTransfer message) to which the user transfers the kudos. I think the advantage of this is that the bank need not know anything about the intent semantics, and we need not fix them ahead of time in the core bank/kudos application – they can be “built on top”. Would that be compatible with the other elements of your proposal?

In my sketch the Bank doesn’t need to know anything about the intents except that they exist. The only thing in the sketch that depends on the definition of the Bank object is the RL of conditionalTransfer, which only needs to check that there is an intent resource with the right hash – it doesn’t need to know anything about the intent except the hash. So a user can create arbitrary intents independently and just provide their hash in an authorised conditionalTransfer message.

I don’t understand the exact role of an ephemeral object. I guess this is supposed to be some sort of intermediary that is trusted by the user with their funds. But what does it do exactly? It needs to somehow use the interface of the Bank to do the transfers, and we’re back at the same problem of ensuring that the counterparty does their expected transfer too. So it seems we have the same thing as above but with EmphemeralObjectA and EphemeralObjectB instead of A and B.

1 Like

Yes, but the bank still has to know about a conditionalTransfer message (in addition to the existing transfer) message, right? I see that this isn’t specific to the details of the intent – so one “conditionalTransfer” would be sufficient for any sort of future intent – but it is still (slightly) more complicated than a bank object which only needs to support the transfer message.

In general, the idea is to encode the user’s authorization of the intent by having the user transfer their kudos to an ephemeral object which itself encodes the intent logic (as opposed to having the user sign over an intent in a separate manner – since transfer itself already checks authorization by signature, we can reuse this, and embed the user’s choice of which intent to authorize by what ephemeral object they transfer their kudos to).

Yes, that’s right – the ephemeral intent object would (a) call transfer to send the (A) kudos from itself to the counterparty (so this message can only be fully determined when the counterparty is known), (b) transfer (B kudos) from itself to the original user, and (c) require that the counterparty transferred (B kudos) to itself (the ephemeral intent object), and this “require” bit would be done in the same way as we’ve been discussing here – I guess vis-a-vis your latest proposal an ephemeral object is just a different way to encode the authorization that we want.

2 Likes

Yes, but the bank still has to know about a conditionalTransfer message (in addition to the existing transfer ) message, right? I see that this isn’t specific to the details of the intent – so one “conditionalTransfer” would be sufficient for any sort of future intent – but it is still (slightly) more complicated than a bank object which only needs to support the transfer message.

Yes, that’s right. Any method that’s supposed to be used by intents needs to have its “conditional” variant. However, how such conditional methods are defined and compiled is uniform and can be built into the translation strategy. The user (defining a Bank in this case) would only indicate with some keyword that a given method can be used in an intent.

the ephemeral intent object would (a) call transfer to send the (A) kudos from itself to the counterparty (so this message can only be fully determined when the counterparty is known), (b) transfer (B kudos) from itself to the original user, and (c) require that the counterparty transferred (B kudos) to itself (the ephemeral intent object), and this “require” bit would be done in the same way as we’ve been discussing here – I guess vis-a-vis your latest proposal an ephemeral object is just a different way to encode the authorization that we want.

I don’t see the advantage of having an ephemeral object, because it seems we still need conditionalTransfer in the Bank interface to do the “require”, in essentially the same way. Or both parties need to trust a single ephemeral intent object, so they could equally well trust the solver.

Let’s say Intent is the ephemeral intent object that A transferred their funds to (with ordinary non-conditional transfer). What does Intent need to do according to the steps above?

  1. transfer(Intent, A, denomB, amountB)
  2. transfer(Intent, B, denomA, amountA)
  3. require transfer(B, Intent, denomB, amountB)

How is the “require” implemented?

If it’s done using a conditionalTransfer, then I don’t see why we need Intent as an intermediary - in comparison to my sketch above we still have essentially the same change in Bank’s interface and we’re only adding an additional layer of complexity.

If B is supposed to just do an ordinary transfer to Intent, then it needs to trust Intent - we’re not really enforcing (from the point of view of B) that Intent does the transfer in step 2. So we have an Intent which both A and B trust with their funds. They could equally well transfer their funds to the Solver and trust it to submit the right transaction.

Or maybe something else is meant here? What I don’t see is how to enforce “require” without something like conditionalTransfer in the interface of Bank and without trusting any intermediaries.

To see more clearly why I think we need conditionalTransfer let’s consider a more naive approach, where A creates IntentA that does the transfer from A to B and checks if there is a required transfer from B to A. Analogously, B creates IntentB.

The RLs of IntentA and IntentB get triggered on consumption of corresponding intent resources and they implement the “require”.

  • RL of IntentA checks if there is a consumed message resource for transfer(_, A, denomB, amountB).
  • RL of IntentB checks if there is a consumed message resource for transfer(_, B, denomA, amountA).

How do we create a transaction?

On the side of IntentA, a partial transaction can be created which consumes transfer(IntentA, B, denomA, amountA) and IntentA. Analogously for IntentB.

Now somebody, an Assembler, needs to combine the partial transactions to create and submit a transaction with transfer(IntentA, B, denomA, amountA), transfer(IntentB, A, denomB, amountB), IntentA, IntentB. But if Assembler is malicious, they can simply pick only e.g. transfer(IntentA, B, denomA, amountA) from the partial transaction they got from IntentA and submit this as a complete transaction. It seems we either need to trust Assembler or somehow bind the transfer with the intent.

conditionalTransfer ensures that the transfer is bound with the intent logic and nobody can simply omit the intent resource. I don’t see how to guarantee this without some modification to the transfer resource logic.

1 Like

In principle, I don’t think this should be necessary – all that “require” needs to check is that a particular message was sent to the Bank object in some action, and that that message was not used to satisfy any other require constraint. It seems to me like we could encode such a constraint as a resource which (a) goes in the action where the transfer message is sent to the Bank object, and (b) checks that the transfer message was in fact sent to the Bank object and that no other constraint checks exist in that action. At the AVM / message sequence chart level of abstraction (paging @graphomath), this is just a linear constraint over a part of the MSC for a particular transaction.

This is a good point – I think you’re right that, in my model, the assembler/solver could indeed just take the partial transaction which transfers kudos to IntentA, and submit that to the controller, which probably isn’t what A wanted. Note that the assembler can’t take the kudos or do anything else, but they can submit a partial state transaction which A didn’t want to be executed (at least in this case – sometimes submitting intents to a controller might be helpful).

However, I still think we can fix this without requiring something like conditionalTransfer. Suppose that we have a slightly more sophisticated version of bank/kudos with receive authorization (so the receiver must authorize any receipt of kudos). The receive authorization logic of IntentA could require that the constraint resource is included in the action (which would then mean that a partial transaction with just transfer(IntentA, B, denomA, amountA) is not accepted).

1 Like

It seems to me like we could encode such a constraint as a resource which (a) goes in the action where the transfer message is sent to the Bank object, and (b) checks that the transfer message was in fact sent to the Bank object and that no other constraint checks exist in that action.

My only concern is ensuring that this constraint resource (which is similar to the intent resources I outline above) is guaranteed to be bound with the transfer, i.e., that some third party handling this can’t simply omit the constraint resource. That’s the only purpose of conditionalTransfer - if we can ensure this by other means then it’s not needed.

Suppose that we have a slightly more sophisticated version of bank/kudos with receive authorization (so the receiver must authorize any receipt of kudos). The receive authorization logic of IntentA could require that the constraint resource is included in the action (which would then mean that a partial transaction with just transfer(IntentA, B, denomA, amountA) is not accepted).

My conditionalTransfer is essentially transfer with sender authorization, i.e., where the sender can authorize the transfer based on arbitrary sender-specified condition (like the presence of a matching reciprocal transfer).

It’s not yet clear to me how receiver authorization would work and how it would help in comparison to sender authorization. Specifically, what incentive does the receiver have to not authorize a transaction? After all, they’re receiving the kudos, so they can always just accept it?

1 Like

Yes, agreed.

Receiver authorization helps here just because it would allow the sender to create an intent object which they will transfer the kudos to, where the intent object’s receiver authorization will be checked, and the intent object’s receiver authorization will check for the presence of the constraint resource (thereby guaranteeing that the constraint resource is bound with the transfer, as we seek).

In a certain sense, one could say, we can derive sender authorization from receiver authorization, since the sender can always create a (perhaps temporary) receiver who will check some arbitrary constraint. We cannot, however, derive receiver authorization from sender authorization, since the receiver does not pick the sender. I think this is a pretty strong reason to prefer receiver authorization as the basic capability, and implement sender authorization in terms of receiver authorization when we need it, since receiver authorization is more fundamental.

1 Like

Receiver authorization helps here just because it would allow the sender to create an intent object which they will transfer the kudos to, where the intent object’s receiver authorization will be checked, and the intent object’s receiver authorization will check for the presence of the constraint resource (thereby guaranteeing that the constraint resource is bound with the transfer, as we seek).

In a certain sense, one could say, we can derive sender authorization from receiver authorization, since the sender can always create a (perhaps temporary) receiver who will check some arbitrary constraint.

Let me try to unpack this and make it more precise and closer to an RM implementation.

We have a sender S and a receiver R. Sender S wants to transfer X to R under the condition that R transfers Y to S. Of course, S can’t rely on receiver authorization for R, because R could just authorize S’s transfer ignoring the condition. So the idea is that S sends X to I (an intent object) that S themselves created, and I (or an associated constraint resource) executes appropriate authorization logic.

But how does this work on the RM level? How does R ultimately receive X? It seems I needs to transfer it to R. How does it ensure that R transfers Y to S? The RL of I could check that there is a transfer message for Y in the action. But how does R ensure that they receive X? They need to do an analogous thing and transfer Y to I' that they trust which does the check for a transfer of X to R.

The difference with the naive example is that now the final transaction also contains the transfers to I and I' and the transfer logic is modified to check for the presence of I (resp. I'). For simplicity, I’m identifying I with the resource which checks the constraints, but they don’t need to be the same.

So the final transaction has:

  • transfer(S, I, X) which checks that I is present.
  • transfer(R, I', Y) which checks that I' is present.
  • I which checks that S receives Y.
  • I' which checks that R receives X.
  • transfer(I, R, X).
  • transfer(I', S, Y).

Now it’s not possible to omit anything because the RLs of other messages would be violated.

The only remaining question is how does the transfer check that I (or corresponding constraint resource C) is present. But that’s analogous to conditionalTransfer - we can have an additional argument with hash(C), or this hash can be stored directly in I.

So it seems we do need a KudosBank interface modification similar to conditionalTransfer. The main difference seems to be that there is no need for exchanging partial messages - we’ve broken up each transfer into two, with the intent object acting as an intermediary.

In fact, we could write this using the old conditionalTransfer:

  • conditionalTransfer(S, I, X, hash(I)) which checks that I is present.
  • conditionalTransfer(R, I', Y, hash(I')) which checks that I' is present.
  • I which checks that S receives Y.
  • I' which checks that R receives X.
  • transfer(I, R, X).
  • transfer(I', S, Y).

I agree that receiver authorization is more general, but I don’t see why anyone would need receiver authorization which is not in effect sender authorization like above (we’re just using an ephemeral receiver intermediary that is fully controlled and trusted by the sender). If the authorization is connected with the actual receiver, not an intermediary the sender controls, then the receiver can always authorize anything because they’re receiving the funds anyway, so why would they care about any extra conditions. The only possible use-case I can think of is when someone wants to prevent receiving funds from some “bad actors”.

Summarizing, from what I understand the main difference is actually the presence of ephemeral intermediaries I and I'. We need something like conditionalTransfer in the Bank interface, regardless if it’s doing sender or receiver authorization.

So the question comes down to: is it better in some sense with the intermediary? I’m actually not sure either way. On one hand, we have an extra intent object, and possibly extra constraint resources. On the other hand, we don’t need to send around partial transfer messages. Perhaps the version with ephemeral intermediaries has the advantage of being closer to a naive example, hence easier to understand.

1 Like

If this hash is stored directly in I, why do we need an additional argument / interface modification? KudosBank doesn’t need to know about anything internal to I (it does need to check receiver authorization, but that’s it).

Yes, this may be useful, and also just the general case of anti-spam – I don’t want to need to scan for any spam funds using shielded sync, for example, as this takes computational resources.

I think it could still be just transfer – regular transfer would perform receive authorization.

1 Like

If this hash is stored directly in I, why do we need an additional argument / interface modification? KudosBank doesn’t need to know about anything internal to I (it does need to check receiver authorization, but that’s it).

Because transfer needs to check that this constraint / intent resource is present, regardless if the hash is stored in I or provided as an argument. In this sense it’s interface modification, because either we add an argument or require I to have some field.

We need to check the presence of C/I in transfer, because otherwise there’s no coupling and the receiver authorization is not enforced. How would transfer perform receiver authorization otherwise, if the receiver can specify it independently of the bank? It seems there needs to be a resource whose RL performs the receiver authorization, and transfer needs to make sure that this resource is present. It needs to be specified somehow to the transfer what this resource is if it is to check that it’s present.

Or we could have some general method of identifying the constraint resource based on the identity of the receiver, and that’s how the bank checks on transfer if this resource is present.

Then it depends what we mean by “interface modification”, because technically we modify how transfer can be used, even if hash / identity of the constraint resource is not provided explicitly but computed based on the identity of the receiver.