Proposal: HTLC and Atomic Swaps with Claimable Balances

116 views
Skip to first unread message

Leigh McCulloch

unread,
Jan 26, 2021, 3:13:48 PMJan 26
to Stellar Developers
Hi all,

I propose that we extend the predicates available to Claimable Balances, adding a new predicate CLAIM_PREDICATE_SIGNER_HASH_X that requires that a ClaimClaimableBalanceOp be accompanied by a SIGNER_KEY_TYPE_HASH_X signature containing x.

```diff
diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x
index 8d746391..7b09302f 100644
--- a/src/xdr/Stellar-ledger-entries.x
+++ b/src/xdr/Stellar-ledger-entries.x
@@ -291,7 +291,8 @@ enum ClaimPredicateType
     CLAIM_PREDICATE_OR = 2,
     CLAIM_PREDICATE_NOT = 3,
     CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME = 4,
-    CLAIM_PREDICATE_BEFORE_RELATIVE_TIME = 5
+    CLAIM_PREDICATE_BEFORE_RELATIVE_TIME = 5,
+    CLAIM_PREDICATE_SIGNATURE_HASH_X = 6
 };
 
 union ClaimPredicate switch (ClaimPredicateType type)
@@ -309,6 +310,8 @@ case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME:
 case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME:
     int64 relBefore; // Seconds since closeTime of the ledger in which the
                      // ClaimableBalanceEntry was created
+case CLAIM_PREDICATE_SIGNATURE_HASH_X:
+    uint256 hashX; // Hash of random 256 bit preimage X
 };
 
 enum ClaimantType
```

This would function similar to how the account hash(x) account signer works:
First, create a random 256 bit value, which we call x. The SHA256 hash of that value can be added as a predicate of type hash(x). Then in order to authorize a claim, the claimant must include x as one of the signatures of the transaction. X will only be usable by the claimant that it is included as a predicate, so exposure of x does not allow any individual to issue a claim if the transaction was to fail and the claimant needed to reissue the claim. Transactions do not need to be created upfront for the claims, they can be constructed after the fact and take any form.

Hash time lock contracts are useful for atomic swaps involving another chain. This change to claimable balances allows claimable balances to be used in atomic swaps.

Example:
Account AA wishes to send Asset A1 to Account AB on Stellar. Account AB wishes to send Asset A2 to Account AA on OtherChain. Both accounts do not trust each other and the swap needs to occur such that once either party can take the other's asset, both can.

1. Account AA randomly generates a value for x
2. Account AA submits transaction CreateClaimableBalanceOp: SourceAccount: AA, Asset: A1, Amount: 1, Claimants: [AA with predicate: not before 2 days from now, AB with predicate: hash(x)]
3. Account AB performs a similar operation on OtherChain with asset A2 with timeout 1 day using hash(x)
4. Account AA claims asset on OtherChain using x, revealing x
5. Account AB claims the asset on Stellar with ClaimClaimableBalanceOp: SourceAccount: AB, ClaimableBalanceID: id, Signature: [AB signers, x]

Stellar already supports atomic swaps with time bound pre-authorized transactions and temporary accounts that are locked and therefore have a predefined set of outcomes. One of the pre-authorized transactions also requires the x signature to lock its use to the revealing of x. Using this approach is complex. The author of the transactions must ensure that the pre-authorized transactions won’t fail, or prepare duplicate sets of the transactions for retries. The author is also required to create a temporary account and handle the recovery of minimum account reserves and the creation and destruction of trustlines in these transactions.

Example:
1. Account AA randomly generates a value for x
2. Account AA generates a temporary account address AT and key
3. Account AA creates account AT depositing sufficient account reserves for 1 trustline, 3 signers
4. Account AA creates trustline on AT for A1
5. Account AA gets next sequence number SN for AT
6. Account AA creates TX1 (SN+1) “pay A1 to AB, remove trustline for A1, remove TX2 pre-authorized signer, account merge to AA”
7. Account AA creates TX2 (SN+1) “pay A1 to AA, remove trustline for A1, remove TX1 pre-authorized signer, remove hash(x) signer, account merge to AA, timebound to 2 days from now”
8. Account AA submits a SetOptionsOp (SN+0) for AT “remove master key signer, set thresholds to low:2 med:2 high:2, add pre-authorized signer for TX1 weight:1, add hash(x) signer weight:1, add pre-authorized signer for TX2 weight:2.
9. Account AB performs a similar operation on OtherChain with asset A2 with timeout 1 day using hash(x)
10. Account AA claims asset on OtherChain using x, revealing x.
11. Account AB claims the asset on Stellar by adding the x signature to TX2, wrapping the transaction in a fee bump transaction to supply the transaction fee, and submitting it to the network.

It is significantly more complicated to create the pre-authorized transactions and ensure they will not fail. Decisions must be made about how to handle the account reserve and where to send it. If the account reserve is intended to be returned to the sending account the second transaction can fail locking up the asset indefinitely unless the author has prepared retry pre-authorized transactions. There are different approaches for how to handle this like leaving the reserve in the locked account, or sending it to the destination.

These issues, especially the issues relating to the management of the temporary account, are similar to the issues anchors experience sending assets to accounts that do not yet hold trustlines. Claimable balances were introduced to make it simpler for anchors to issue assets to new accounts in protocol 15 and this small predicate extension brings similar wins to atomic swaps as well.

I think this would simplify atomic swaps on Stellar. Are there approaches simpler than what I outline as currently possible without this change? Thoughts?

I’d be happy to write up a formal proposal in the near future if this is of interest.

Leigh

Jonathan Jove

unread,
Jan 26, 2021, 4:24:38 PMJan 26
to Leigh McCulloch, Stellar Developers
I like this idea Leigh. I think it's a perfect way to use the extensibility of ClaimableBalance predicates to achieve interesting goals. One technical detail here: we keep signatures pretty separate from operation processing. For example, this approach would add complexity to checking for `txBAD_AUTH_EXTRA`.

I propose we put the preimage into an operation. I have imagined other situations where claiming a ClaimableBalance might require some kind of "payload". I think we should add a new operation that can handle payloads in a uniform way. For example,

```
enum ClaimPayloadType
{
    CLAIM_PAYLOAD_HASH_PREIMAGE
};

union ClaimPayload switch (ClaimPayloadType type)
{
case CLAIM_PAYLOAD_HASH_PREIMAGE:
    uint256 preimage;
};

struct ClaimClaimableBalanceWithPayload
{
    ClaimableBalanceID balanceID;
    ClaimPayloadType payload<10>;
};
```

The operation would be entirely analogous to `ClaimClaimableBalance` except that it would attempt to use the payload to satisfy certain predicates. How do you feel about something like this?

--
You received this message because you are subscribed to the Google Groups "Stellar Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to stellar-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/e7d70836-8699-46c3-9d55-2476865d4c2fn%40googlegroups.com.

Leigh McCulloch

unread,
Jan 26, 2021, 5:50:25 PMJan 26
to Stellar Developers
Thanks Jon. The addition of a new operation to store the payload makes sense. I also like the idea to support multiple payloads upfront.

What happens if you provide a payload when one is not required? I would think it would be ideal for the transaction to fail in the same way an unnecessary signature will fail. Do you agree?

Jonathan Jove

unread,
Jan 27, 2021, 12:04:51 PMJan 27
to Leigh McCulloch, Stellar Developers
I gave this question some thought when I was drafting my previous comment. It's pretty tricky. I think some examples will illustrate the kinds of scenarios we have to consider.

Let x and y be 256-bit values. Suppose the predicate is HASH(x) || HASH(y). Then technically, it should be valid to provide Payload(x), Payload(y), or Payload(x, y) because all of these will satisfy the predicate. But strictly speaking, the third one contains extra information. Should it be invalid? If it is invalid, then we must evaluate the predicates using short-circuiting (this is how we implemented predicates, but that has no observable consequences today) because otherwise both x and y would be used.

Let x be a 256-bit value and t a time. Suppose the predicate is BEFORE(t) || HASH(x). One might reasonably assume that having a pre-signed transaction with Payload(x) should always enable claiming. But if we evaluate the predicates using short-circuiting and forbid extra payloads, then that pre-signed transaction actually _does not_ work before t because in that case it has an extra payload. On the other hand, with the predicate HASH(x) || BEFORE(t) that pre-signed transaction would always enable claiming. In other words, short-circuiting plus forbidding extra payloads means that the order of the predicates becomes critically important.

It would be possible to not use short-circuiting and then forbid purely extraneous payloads. For example, in the first example above we could accept Payload(x, y) but reject Payload(x, z) where z is some other 256-bit value. I'm not sure how much value this adds because it forces stellar-core to actually determine that z is purely extraneous by exploring the entire predicate tree.

With all of this said, it is not obvious how to forbid extra payloads in a way that is consistent, easy to reason about, and valuable. Is there some significant benefit that warrants further research into this topic, or should we simply allow extra payloads?

As a slight aside, one other way we could structure the payloads is by putting them into a tree analogous to the predicate tree. Then we know which payload applies to which predicate. This would potentially be much more computationally efficient from a stellar-core perspective. It would also allow us to enforce the condition that certain predicates must not have payloads attached to them, which is a facsimile of the no extra payloads condition that Leigh mentioned.

Leigh McCulloch

unread,
Jan 27, 2021, 3:22:01 PMJan 27
to Stellar Developers
After reading your examples I think we should allow extra payloads, which keeps the operation simpler for clients to use and reduces failure cases that are difficult to predict, like the one you provided BEFORE(t) || HASH(x).

Could you share an example of what the ClaimClaimableBalanceWithPayloadOp would look like using the tree model? It sounds interesting, but may complicate clients. It's possible a client might support claims for a common case, but may not handle constructing the tree for a more complex configuration that their case is a subtree of. Forcing the client to need to know all the predicates of the claim to structure its claim would not be ideal, so I think the simpler allow approach is preferred.

Nicolas Barry

unread,
Jan 28, 2021, 2:42:53 PMJan 28
to Leigh McCulloch, Stellar Developers
This got complicated quickly :)

A few questions, in priority order.

# Is this the right problem to solve?
I don't know the details of the smart contract you're trying to develop. This particular change may only help a small subset of smart contracts, or only help this very specific situation.
Worst, it can increase the complexity in all SDKs and client code regardless of their use of that feature (new envelope types have a lot of implications).
If the problem is that it's hard to deal with transactions that fail, I think we can think of ways to address that?
We can do this with protocol changes, or by developing "verifiers" or even "debuggers" for smart contracts to help people check that their protocol is sound.

# Approach wrt "BalanceEntry"
Note that if we only care about a single hash, we can already attach a memo with type `MEMO_HASH`.

I don't like that we're scaffolding signature schemes. Right now we need hash preimage, in the future we'll need something else like specific signatures. This probably creates new problems (malleability issues in particular come to mind) - maybe there is a way to fix this as part of the "confused deputy" type of problems instead of creating a one off solution for balance entries.

If we want to push forward with flows that "never fail" via "Balance Entry", I think that the best opportunity on that front is to use them to support "UTXO" style flows instead of trying to shoehorn that into the existing transaction types (this thread already proposed adding a new Transaction type anyways).
Basically, take advantage of the fact that "BalanceEntry" have globally unique identifiers, which can act both as input to some transaction, but also as replay protection and new balance entries generated by such a transaction are have a fully deterministic ID (so we can have flows that only depend on balance entries, and don't depend on any other ledger entry).

Nicolas

Leigh McCulloch

unread,
Jan 28, 2021, 3:13:59 PMJan 28
to Stellar Developers
Jon, one really big limitation of placing the preimage in the operation payload is that the preimage cannot be attached later to a preauthorized/presigned transaction, and could be inadvertently revealed if the claiming transaction is generated ahead of time and shared with other parties to transparently disclose the steps in the contract. For this reason I'd favor keeping it outside. What do you think?

Nico, I don't think this change requires a new envelope type. What part of the change will require that? The original proposal adds a new predicate which relies on a new use of an existing signature type, but otherwise the transaction is unaltered and the changes to SDKs is minimal.

I have also been thinking about the Claimable Balance as the mechanism to support UTXO style flows as well. I'd like to explore that more but I need to read through more prior discussions about that. The approach I had been thinking about, which is likely flawed, is the introduction of a CreateEphemeralAccountOp that creates an account that is limited to the existing transaction, has no sequence number, trustlines, flags, can hold any balance placed in it, and at the end of the transaction must be empty – outputs must equal inputs. Pre-authorized transactions today must understand all possible states of the world when moving funds out of a claimable balance and into an existing or new account, but with an ephemeral account, they would not need to predict the state of the world to split up funds from a claimable balance and could make use of existing. Another approach would be a new operations for claiming the claimable balance and redistributing them within the transaction, but this wouldn't build on any existing operations we already have. I'm continuing to think about this.

Is anyone else thinking about Claimable Balances as a mechanism to support UTXO style flows? Should we setup a working group to start pulling together ideas, and figure out if that is something to pursue?

Jonathan Jove

unread,
Jan 29, 2021, 11:06:01 AMJan 29
to Leigh McCulloch, Stellar Developers
That's an interesting point about attaching the private data later. I hadn't considered that use case. I'll need to give that more thought.

The concept of an ephemeral account is intriguing, but I have a lot of questions before I can really evaluate such an idea. The two most important categories are:
- How does an ephemeral account interact with claimable balances? Can it claim anything that the creating account could have claimed?
- How does an ephemeral account interact with authorization? Or do we make the restriction that an asset must not be authorization required in order to send it to an ephemeral account (I don't think this works great because that could change unless authorization immutable is set)?

Perhaps I am missing some important detail about this idea.

Leigh McCulloch

unread,
Jan 29, 2021, 11:32:02 AMJan 29
to Stellar Developers
Regarding ephemeral accounts, Nico mentioned to me that we have explored this before and there were significant complex issues, and so other more direct approaches for claimable balance to claimable balance movement of assets not via accounts are probably easier. They're probably easier to understand too for the user. e.g. SplitClaimableBalanceOp that claims a claimable balance and moves the asset directly into two or more new claimable balances. I think Nico has some ideas for how this could work efficiently.

I think CB -> CB asset flows would be very useful, but I don't think it replaces the usefulness of hash(x) predicates on CB. Both of these things would be useful. The hash(x) predicate could improve CB usefulness for smaller contracts like atomic swaps.

Nico, do you think these things are inherently bound together and need to be explored together?

Nicolas Barry

unread,
Jan 29, 2021, 11:52:06 AMJan 29
to Leigh McCulloch, Stellar Developers
I don't think you addressed my questions Leigh regarding what problem we're actually trying to solve here:
it seems to me that this is overly focused on the HTLC construct in the context of a cross chain setup which is maybe harder than it should be but doable with what exists today on the network.
I was hoping you would be looking into more complicated protocols like rollups in channels that are very hard to get right as we saw before with Starlight and can't rely on just HTLCs from what I remember (we used a ratchet account to allow skip aheads).

Nicolas


Leigh McCulloch

unread,
Jan 29, 2021, 12:28:31 PMJan 29
to Stellar Developers
I'm looking into Starlight and the transactions it builds, and channels in general, but not ready to discuss it in detail yet. This conversation was originally intended to be scoped to the atomic swap use case as one case of low hanging fruit, but I see your point.

I'll circle back here when I have a more complete view of the complex cases.
Reply all
Reply to author
Forward
0 new messages