CAP: 0023
Title: Two-Part Payments with ClaimableBalanceEntry
Author: Jonathan Jove
Status: Final
Created: 2019-06-04
Updated: 2020-02-05
Discussion: https://github.com/stellar/stellar-protocol/issues/303
Protocol version: 14
Payments can fail depending on the state of the destination account. This proposal introduces new operations that separate sending a payment from receiving the payment. Then the success of sending depends only on the state of the sending account and success of receiving depends only on the state of the receiving account.
This proposal seeks to solve the following problem: it should be easy to send a payment to an account that is not necessarily prepared to receive the payment. There are several manifestations of this problem, the two most important being
- it should be easy for protocols (like an implementation of payment channels) to pay out to participants, and
- it should be easy for issuers to issue assets non-interactively.
This proposal is aligned with several Stellar Network Goals, among them:
- The Stellar Network should facilitate simplicity and interoperability with other protocols and networks.
- The Stellar Network should enable cross-border payments, i.e. payments via
exchange of assets, throughout the globe, enabling users to make payments
between assets in a manner that is fast, cheap, and highly usable.
- In support of this, the Stellar Network should enable asset issuance, but as a means of enabling cross-border payments.
We introduce ClaimableBalanceEntry as a new type of LedgerEntry which
represents the transfer of ownership of some amount of an asset. The operations
CreateClaimableBalanceOp and ClaimClaimableBalanceOp allow the creation and
consumption of claimable balance entries, which permits temporal separation of
initiating and reciving a payment. Existing proposals, such as those for
deterministic accounts, can provide a similar mechanism but are not able to
handle authorization restricted assets as easily. A specific and simple protocol
that will be facilitated is the asset issuance protocol that issues an asset to
a given account, regardless of whether it exists or is prepared to receive the
funds.
First, we introduce ClaimableBalanceEntry(Note that the XDR was updated in https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#claimablebalanceentry) and the corresponding changes for
LedgerEntryType and LedgerEntry.
enum LedgerEntryType
{
// ... ACCOUNT, TRUSTLINE, OFFER, unchanged ...
DATA = 3,
CLAIMABLE_BALANCE = 4
};
enum ClaimPredicateType
{
CLAIM_PREDICATE_UNCONDITIONAL = 0,
CLAIM_PREDICATE_AND = 1,
CLAIM_PREDICATE_OR = 2,
CLAIM_PREDICATE_NOT = 3,
CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME = 4,
CLAIM_PREDICATE_BEFORE_RELATIVE_TIME = 5
};
union ClaimPredicate switch (ClaimPredicateType type)
{
case CLAIM_PREDICATE_UNCONDITIONAL:
void;
case CLAIM_PREDICATE_AND:
ClaimPredicate andPredicates<2>;
case CLAIM_PREDICATE_OR:
ClaimPredicate orPredicates<2>;
case CLAIM_PREDICATE_NOT:
ClaimPredicate* notPredicate;
case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME:
int64 absBefore; // Will return true if closeTime < absBefore
case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME:
int64 relBefore; // Seconds since closeTime of the ledger in which the
// ClaimableBalanceEntry was created
};
enum ClaimantType
{
CLAIMANT_TYPE_V0 = 0
};
union Claimant switch (ClaimantType type)
{
case CLAIMANT_TYPE_V0:
struct {
AccountID destination; // The account that can use this condition
ClaimPredicate predicate; // Claimable if predicate is true
} v0;
};
enum ClaimableBalanceIDType
{
CLAIMABLE_BALANCE_ID_TYPE_V0 = 0
};
union ClaimableBalanceID switch (ClaimableBalanceIDType type)
{
case CLAIMABLE_BALANCE_ID_TYPE_V0:
Hash v0;
};
struct ClaimableBalanceEntry
{
// Unique identifier for this ClaimableBalanceEntry
ClaimableBalanceID balanceID;
// Account that created this ClaimableBalanceEntry
AccountID createdBy;
// List of claimants with associated predicate
Claimant claimants<10>;
// Any asset including native
Asset asset;
// Amount of asset
int64 amount;
// Amount of native asset to pay the reserve
int64 reserve;
// reserved for future use
union switch (int v)
{
case 0:
void;
}
ext;
};
struct LedgerEntry
{
uint32 lastModifiedLedgerSeq; // ledger the LedgerEntry was last changed
union switch (LedgerEntryType type)
{
// ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
case CLAIMABLE_BALANCE:
ClaimableBalanceEntry claimableBalance;
}
data;
// reserved for future use
union switch (int v)
{
case 0:
void;
}
ext;
};
struct LedgerKey
{
// ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
case CLAIMABLE_BALANCE:
struct
{
ClaimableBalanceID balanceID;
} claimableBalance;
};Second, we introduce the new operations CreateClaimableBalanceOp and
ClaimClaimableBalanceOp as well as the corresponding changes to
OperationType and Operation. We also introduce the type OperationID
to represent the hash preimage for ClaimableBalanceID, along with a new EnvelopeType.
enum OperationType
{
// ... CREATE_ACCOUNT, ..., MANAGE_BUY_OFFER unchanged ...
PATH_PAYMENT_STRICT_SEND = 13,
CREATE_CLAIMABLE_BALANCE = 14,
CLAIM_CLAIMABLE_BALANCE = 15
};
struct CreateClaimableBalanceOp
{
Asset asset;
int64 amount;
Claimant claimants<10>;
};
struct ClaimClaimableBalanceOp
{
ClaimableBalanceID balanceID;
};
struct Operation
{
// sourceAccount is the account used to run the operation
// if not set, the runtime defaults to "sourceAccount" specified at
// the transaction level
AccountID* sourceAccount;
union switch (OperationType type)
{
// ... CREATE_ACOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
case CREATE_CLAIMABLE_BALANCE:
CreateClaimableBalanceOp createClaimableBalanceOp;
case CLAIM_CLAIMABLE_BALANCE:
ClaimClaimableBalanceOp claimClaimableBalanceOp;
}
body;
};
union OperationID switch (EnvelopeType type)
{
case ENVELOPE_TYPE_OP_ID:
struct
{
AccountID sourceAccount;
SequenceNumber seqNum;
uint32 opNum;
} id;
};
enum EnvelopeType
{
// ... ENVELOPE_TYPE_TX_V0, ..., ENVELOPE_TYPE_TX_FEE_BUMP unchanged ...
ENVELOPE_TYPE_OP_ID = 6
};Third, we introduce the result types CreateClaimableBalanceResult and
ClaimClaimableBalanceResult as well as the corresponding changes to
OperationResult.
enum CreateClaimableBalanceResultCode
{
CREATE_CLAIMABLE_BALANCE_SUCCESS = 0,
CREATE_CLAIMABLE_BALANCE_MALFORMED = -1,
CREATE_CLAIMABLE_BALANCE_LOW_RESERVE = -2,
CREATE_CLAIMABLE_BALANCE_NO_TRUST = -3,
CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -4,
CREATE_CLAIMABLE_BALANCE_UNDERFUNDED = -5
};
union CreateClaimableBalanceResult switch (CreateClaimableBalanceResultCode code)
{
case CREATE_CLAIMABLE_BALANCE_SUCCESS:
ClaimableBalanceID balanceID;
default:
void;
};
enum ClaimClaimableBalanceResultCode
{
CLAIM_CLAIMABLE_BALANCE_SUCCESS = 0,
CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST = -1,
CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM = -2,
CLAIM_CLAIMABLE_BALANCE_LINE_FULL = -3,
CLAIM_CLAIMABLE_BALANCE_NO_TRUST = -4,
CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -5
};
union ClaimClaimableBalanceResult switch (ClaimClaimableBalanceResultCode code)
{
case CLAIM_CLAIMABLE_BALANCE_SUCCESS:
void;
default:
void;
};
struct OperationResult
{
// sourceAccount is the account used to run the operation
// if not set, the runtime defaults to "sourceAccount" specified at
// the transaction level
AccountID* sourceAccount;
union switch (OperationType type)
{
// ... CREATE_ACOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
case CREATE_CLAIMABLE_BALANCE:
CreateClaimableBalanceResult createClaimableBalanceResult;
case CLAIM_CLAIMABLE_BALANCE:
ClaimClaimableBalanceResult claimClaimableBalanceResult;
}
body;
};A ClaimableBalanceEntry can only be created by the CreateClaimableBalanceOp
operation. CreateClaimableBalanceOp is invalid with
CREATE_CLAIMABLE_BALANCE_MALFORMED if
assetis invalidamount <= 0claimantshas length 0claimants[i].destination = claimants[j].destination(for anyi != j)claimants[i].predicatehas depth greater than 4 (for anyi)claimants[i].predicatecontains a predicate of typeCLAIM_PREDICATE_ANDwithandPredicates.size() != 2,CLAIM_PREDICATE_ORwithorPredicates.size() != 2, orCLAIM_PREDICATE_NOTwith a nullnotPredicate(for anyi)claimants[i].predicatecontains a predicate of typeCLAIM_PREDICATE_BEFORE_ABSOLUTE_TIMEorCLAIM_PREDICATE_BEFORE_RELATIVE_TIMEwithabsBefore < 0orrelBefore < 0(for anyi)
The behavior of CreateClaimableBalanceOp is as follows:
- Fail with
CREATE_CLAIMABLE_BALANCE_LOW_RESERVEif thesourceAccountdoes not have at leastclaimants.size() * baseReserveavailable balance of native asset - Deduct
claimants.size() * baseReserveof native asset fromsourceAccount - Fail with
CREATE_CLAIMABLE_BALANCE_NO_TRUSTif thesourceAccountdoes not have a trust line forasset - Fail with
CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZEDif thesourceAccounttrust line forassetdoes not haveAUTHORIZED_FLAGset - Fail with
CREATE_CLAIMABLE_BALANCE_UNDERFUNDEDif thesourceAccountdoes not have at leastamountavailable balance ofasset - Deduct
amountofassetfromsourceAccount - Create a claimable balance entry with the following properties:
balanceIDof typeCLAIMABLE_BALANCE_ID_TYPE_V0.1createdBy = sourceAccount(of the transaction, not the operation)claimantsas specified, with the exception thatCLAIM_PREDICATE_BEFORE_RELATIVE_TIMEwill be converted toCLAIM_PREDICATE_BEFORE_ABSOLUTE_TIMEby addingrelBeforeto thecloseTimein theLedgerHeader. If this addition exceedsINT64_MAXthen useINT64_MAX.
assetas specified in the operationamountas specified in the operationreserveequal toclaimants.size() * baseReserve
- Succeed with
CREATE_CLAIMABLE_BALANCE_SUCCESSand thebalanceIDfrom the previous step.
CreateClaimableBalanceOp requires medium threshold because it can be used to
send funds.
A ClaimableBalanceEntry can only be deleted by the ClaimClaimableBalanceOp
operation. ClaimClaimableBalanceOp cannot be invalid.
The behavior of ClaimClaimableBalanceOp is as follows:
- Fail with
CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXISTif there is noClaimableBalanceEntrymatchingbalanceID. - Fail with
CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIMif there is noisuch thatclaimants[i].destination = sourceAccountor ifclaimants[i].predicateis not satisfied - Skip to step 7 if
createdBydoes not exist - Skip to step 7 if
createdBydoes not have at leastreserveavailable limit of native asset - Add
reserveof native asset tocreatedBy - Skip to step 9
- Fail with
CLAIM_CLAIMABLE_BALANCE_LINE_FULLif thesourceAccountdoes not have at leastreserveavailable limit of native asset - Add
reserveof native asset tosourceAccount - Fail with
CLAIM_CLAIMABLE_BALANCE_NO_TRUSTifassetis not of typeASSET_TYPE_NATIVEand thesourceAccounttrust line forassetdoes not exist - Fail with
CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZEDifassetis not of typeASSET_TYPE_NATIVEand thesourceAccounttrust line forassetdoes not have theAUTHORIZED_FLAGflag set - Fail with
CLAIM_CLAIMABLE_BALANCE_LINE_FULLif thesourceAccountdoes not have at leastamountavailable limit ofasset - Add
amountofassetto thesourceAccount - Delete the
ClaimableBalanceEntry - Succeed with
CLAIM_CLAIMABLE_BALANCE_SUCCESS
ClaimClaimableBalanceOp requires low threshold because it can only be used to
transfer funds from a ClaimableBalanceEntry to a trust line.
Each ClaimableBalanceEntry exists as an independent entity on the ledger. It
is clear that a ClaimableBalanceEntry cannot be a sub-entry of any its
claimants, because it is a security risk for accounts to be able to add
sub-entries to other accounts. But why should these entries be independent
entities on the ledger rather than sub-entries of the accounts that created
them? There are two main benefits of this design:
- Sending accounts are not limited in the number of claimable balance entries they can create
- Sending accounts can be merged even if they created claimable balance entries that have not yet been claimed
For each ClaimableBalanceEntry, claimants contains a finite and immutable
list of accounts that could potentially claim the ClaimableBalanceEntry. Even
if the conditions are satisfiable (which is not guaranteed), it is still
possible for the ClaimableBalanceEntry to become stranded. If all of the
accounts listed in claimants are merged and none of the private keys are
known, then the ClaimableBalanceEntry will no longer be claimable.
Suppose that we try to relax this requirement in order to avoid this downside.
We could instead make claimants contain a finite and immutable list of public
keys. The operation to claim the ClaimableBalanceEntry could then contain a
signature over the tuple (sourceAccount, balanceID). If the signature was not
from one of the public keys that satisfy the conditions, then the operation
would fail. This would allow the appropriate party to claim the
ClaimableBalanceEntry into any account that they control. But this would also
make it considerably easier to circumvent authorization restrictions on assets.
For instance, an authorized account could create a ClaimableBalanceEntry with
a recipient public key whose private key is known only to some other party. That
party would then control the funds in the ClaimableBalanceEntry and could
claim them into any account that is authorized. A similar scheme could be
executed today by changing the signers on an account, but this would only be
possible once per authorized account and cannot separate out a fraction of the
funds. In summary, an approach that could allow ClaimableBalanceEntry to be
claimable into any account would significantly weaken the strength of
authorization restrictions.
One issue which has been discussed during the development of this proposal is
the absence of a mechanism to increase the amount of a
ClaimableBalanceEntry. The specific scenario which would warrant this
functionality is when a single account sends many identical payments to a single
account that is not prepared to receive them and does not claim any of the
payments. However, this case is sufficiently specific that we recommend pursuing
it in a separate proposal once this proposal has been implemented. Delaying this
feature presents minimal additional difficulty because ClaimableBalanceEntry
has an extension point.
This issue has also been slightly mitigated relative to earlier versions of this
proposal because ClaimableBalanceEntry now returns the reserve to the sending
account, whenever possible.
Everything proposed in this document takes the same stance as existing features of the protocol with regard to memo: memo is a property of a transaction, not of an operation or a ledger entry.
All downstream systems will need updated XDR in order to recognize the new operations and ledger entries.
This proposal will slightly reduce the efficacy of base reserve changes, because
a ClaimableBalanceEntry that has insufficient reserve is still usable.
None yet.
Footnotes
-
HashIDPreimage = ( sourceAccount; seqNum; opNum; ) OperationID = sha256(HashIDPreimage) balanceID.v0() = OperationID // Hash clientDisplayID = hex(HashIDPreimage)HashIDPreimage: a switch within the newtypeENVELOPE_TYPE_OP_IDOperationID: hash ofENVELOPE_TYPE_OP_IDprecomputation datasourceAccount: unmuxed public key of the transaction's sourceseqNum: the transaction source account's sequence numberopNum: position index of this operation in the transaction