Skip to content

Latest commit

 

History

History
458 lines (397 loc) · 16.4 KB

cap-0023.md

File metadata and controls

458 lines (397 loc) · 16.4 KB

Preamble

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

Simple Summary

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.

Motivation

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

  1. it should be easy for protocols (like an implementation of payment channels) to pay out to participants, and
  2. it should be easy for issuers to issue assets non-interactively.

Goals Alignment

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.

Abstract

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.

Specification

XDR

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;
};

Semantics

CreateClaimableBalanceOp

A ClaimableBalanceEntry can only be created by the CreateClaimableBalanceOp operation. CreateClaimableBalanceOp is invalid with CREATE_CLAIMABLE_BALANCE_MALFORMED if

  • asset is invalid
  • amount <= 0
  • claimants has length 0
  • claimants[i].destination = claimants[j].destination (for any i != j)
  • claimants[i].predicate has depth greater than 4 (for any i)
  • claimants[i].predicate contains a predicate of type CLAIM_PREDICATE_AND with andPredicates.size() != 2, CLAIM_PREDICATE_OR with orPredicates.size() != 2, or CLAIM_PREDICATE_NOT with a null notPredicate (for any i)
  • claimants[i].predicate contains a predicate of type CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME or CLAIM_PREDICATE_BEFORE_RELATIVE_TIME with absBefore < 0 or relBefore < 0 (for any i)

The behavior of CreateClaimableBalanceOp is as follows:

  1. Fail with CREATE_CLAIMABLE_BALANCE_LOW_RESERVE if the sourceAccount does not have at least claimants.size() * baseReserve available balance of native asset
  2. Deduct claimants.size() * baseReserve of native asset from sourceAccount
  3. Fail with CREATE_CLAIMABLE_BALANCE_NO_TRUST if the sourceAccount does not have a trust line for asset
  4. Fail with CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED if the sourceAccount trust line for asset does not have AUTHORIZED_FLAG set
  5. Fail with CREATE_CLAIMABLE_BALANCE_UNDERFUNDED if the sourceAccount does not have at least amount available balance of asset
  6. Deduct amount of asset from sourceAccount
  7. Create a claimable balance entry with the following properties:
    • balanceID of type CLAIMABLE_BALANCE_ID_TYPE_V0. balanceID.v0() equal to the SHA256 hash of the OperationID, which is created with the type ENVELOPE_TYPE_OP_ID, sourceAccount of the transaction (not the operation), the seqNum of the transaction, and the index of this operation in the transaction as opNum.
    • createdBy = sourceAccount
    • claimants as specified, with the exception that
      • CLAIM_PREDICATE_BEFORE_RELATIVE_TIME will be converted to CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME by adding relBefore to the closeTime in the LedgerHeader. If this addition exceeds INT64_MAX then use INT64_MAX.
    • asset as specified in the operation
    • amount as specified in the operation
    • reserve equal to claimants.size() * baseReserve
  8. Succeed with CREATE_CLAIMABLE_BALANCE_SUCCESS and the balanceID from the previous step.

CreateClaimableBalanceOp requires medium threshold because it can be used to send funds.

ClaimClaimableBalanceOp

A ClaimableBalanceEntry can only be deleted by the ClaimClaimableBalanceOp operation. ClaimClaimableBalanceOp cannot be invalid.

The behavior of ClaimClaimableBalanceOp is as follows:

  1. Fail with CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST if there is no ClaimableBalanceEntry matching balanceID.
  2. Fail with CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM if there is no i such that claimants[i].destination = sourceAccount or if claimants[i].predicate is not satisfied
  3. Skip to step 7 if createdBy does not exist
  4. Skip to step 7 if createdBy does not have at least reserve available limit of native asset
  5. Add reserve of native asset to createdBy
  6. Skip to step 9
  7. Fail with CLAIM_CLAIMABLE_BALANCE_LINE_FULL if the sourceAccount does not have at least reserve available limit of native asset
  8. Add reserve of native asset to sourceAccount
  9. Fail with CLAIM_CLAIMABLE_BALANCE_NO_TRUST if asset is not of type ASSET_TYPE_NATIVE and the sourceAccount trust line for asset does not exist
  10. Fail with CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED if asset is not of type ASSET_TYPE_NATIVE and the sourceAccount trust line for asset does not have the AUTHORIZED_FLAG flag set
  11. Fail with CLAIM_CLAIMABLE_BALANCE_LINE_FULL if the sourceAccount does not have at least amount available limit of asset
  12. Add amount of asset to the sourceAccount
  13. Delete the ClaimableBalanceEntry
  14. 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.

Design Rationale

ClaimableBalanceEntry is not a sub-entry

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:

  1. Sending accounts are not limited in the number of claimable balance entries they can create
  2. Sending accounts can be merged even if they created claimable balance entries that have not yet been claimed

ClaimableBalanceEntry claimants are accounts

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.

Should it be possible to increase the amount of a ClaimableBalanceEntry?

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.

Memo

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.

Backwards Incompatibilities

All downstream systems will need updated XDR in order to recognize the new operations and ledger entries.

Security Concerns

This proposal will slightly reduce the efficacy of base reserve changes, because a ClaimableBalanceEntry that has insufficient reserve is still usable.

Test Cases

None yet.

Implementation

stellar/stellar-core#2591