Skip to content

Latest commit

 

History

History
387 lines (333 loc) · 14.5 KB

cap-0023.md

File metadata and controls

387 lines (333 loc) · 14.5 KB

Preamble

CAP: 0023
Title:  Two-Part Payments with BalanceEntry
Author: Jonathan Jove
Status: Draft
Created: 2019-06-04
Discussion: https://github.com/stellar/stellar-protocol/issues/303
Protocol version: TBD

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 the following Stellar Network Goal: 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.

Abstract

We introduce BalanceEntry as a new type of LedgerEntry which represents the transfer of ownership of some amount of an asset. CreateBalance and ClaimBalance operation enable the temporal separation of initiating and receiving a payment, respectively. This will facilitate protocols built on the Stellar network by providing a mechanism for payments that never fail. 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 creates an account for another party and issues an asset to that account.

Specification

XDR

We first introduce BalanceEntry and the corresponding changes for LedgerEntryType and LedgerEntry.

enum LedgerEntryType
{
    // ... ACCOUNT, TRUSTLINE, OFFER, unchanged ...
    DATA = 3,
    BALANCE = 4
};

struct BalanceEntry
{
    // If this BalanceEntry was created by operation index I in a transaction
    // with sourceAccount S and seqNum N, then balanceID = SHA256(S, N, I)
    Hash balanceID;

    // Can only be revoked by createdBy
    AccountID createdBy;

    // Can only be claimed by claimableBy
    AccountID claimableBy;

    // Amount of native asset to pay the reserve as of the ledger when this
    // BalanceEntry was created
    int64 reserve;

    // Any asset including native
    Asset asset;

    // Amount of asset
    int64 amount;

    // If authorize is true, authorize claimableBy for asset when this
    // BalanceEntry is claimed
    bool authorize;

    // Cannot be revoked (eg. claimed by createdBy) until after revokeTime
    Timepoint* revokeTime;
};

struct LedgerEntry
{
    uint32 lastModifiedLedgerSeq; // ledger the LedgerEntry was last changed

    union switch (LedgerEntryType type)
    {
    // ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
    case BALANCE:
        BalanceEntry balance;
    }
    data;

    // reserved for future use
    union switch (int v)
    {
    case 0:
        void;
    }
    ext;
};

We next introduce the new operations CreateBalance, ClaimBalance, and AuthorizeBalance and the corresponding changes to OperationType and Operation.

enum OperationType
{
    // ... CREATE_ACCOUNT, ..., BUMP_SEQUENCE unchanged ...
    MANAGE_BUY_OFFER = 12,
    CREATE_BALANCE = 13,
    CLAIM_BALANCE = 14,
    AUTHORIZE_BALANCE = 15
};

struct CreateBalance
{
    AccountID claimableBy;
    Asset asset;
    int64 amount;
    Timepoint* revokeTime;
};

struct ClaimBalance
{
    Hash balanceID;
};

struct AuthorizeBalance
{
    Hash balanceID;
    bool authorize;
};

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, ..., BUMP_SEQUENCE unchanged ...
    case CREATE_BALANCE:
        CreateBalance createBalanceOp;
    case CLAIM_BALANCE:
        ClaimBalance claimBalanceOp;
    case AUTHORIZE_BALANCE:
        AuthorizeBalance authorizeBalanceOp;
    }
    body;
};

We now introduce the result types CreateBalanceResult, ClaimBalanceResult, and AuthorizeBalanceResult and the corresponding changes to OperationResult.

enum CreateBalanceResultCode
{
    CREATE_BALANCE_SUCCESS = 0,
    CREATE_BALANCE_MALFORMED = -1,
    CREATE_BALANCE_NOT_AUTHORIZED = -2,
    CREATE_BALANCE_LOW_RESERVE = -3,
    CREATE_BALANCE_UNDERFUNDED = -4
};

union CreateBalanceResult switch (CreateBalanceResultCode code)
{
case CREATE_BALANCE_SUCCESS:
    void;
default:
    void;
};

enum ClaimBalanceResultCode
{
    CLAIM_BALANCE_SUCCESS = 0,
    CLAIM_BALANCE_DOES_NOT_EXIST = -1,
    CLAIM_BALANCE_CANNOT_CLAIM = -2
    CLAIM_BALANCE_LOW_RESERVE = -3,
    CLAIM_BALANCE_NOT_AUTHORIZED = -4,
    CLAIM_BALANCE_LINE_FULL = -5,
};

union ClaimBalanceResult switch (ClaimBalanceResultCode code)
{
case CLAIM_BALANCE_SUCCESS:
    void;
default:
    void;
};

enum AuthorizeBalanceResultCode
{
    AUTHORIZE_BALANCE_SUCCESS = 0,
    AUTHORIZE_BALANCE_DOES_NOT_EXIST = -1,
    AUTHORIZE_BALANCE_NOT_ISSUER = -2,
    AUTHORIZE_BALANCE_TRUST_NOT_REQUIRED = -3,
    AUTHORIZE_BALANCE_CANT_REVOKE = -4
};

union AuthorizeBalanceResult switch (AuthorizeBalanceResultCode code)
{
case AUTHORIZE_BALANCE_SUCCESS:
    void;
default:
    void;
};

union OperationResult switch (OperationResultCode code)
{
case opINNER:
    union switch (OperationType type)
    {
    // ... CREATE_ACOUNT, ..., BUMP_SEQUENCE unchanged ...
    case CREATE_BALANCE_ENTRY:
        CreateBalanceResult createBalanceResult;
    case CLAIM_BALANCE_ENTRY:
        ClaimBalanceResult claimBalanceResult;
    case AUTHORIZE_BALANCE_ENTRY:
        AuthorizeBalanceResult authorizeBalanceResult;
    }
    tr;
default:
    void;
};

Semantics

CreateBalance

A BalanceEntry can only be created by the CreateBalance operation. The balanceID is generated from the SHA256 hash of the concatenation of the source account of the transaction in which the BalanceEntry was created, the sequence number of the source account, and the index of the CreateBalance operation that created the BalanceEntry. This tuple ensures, up to hash collision, that the balanceID is unique. The createdBy is set to the source account of the CreateBalance operation that created the BalanceEntry. The reserve equals the baseReserve at the time that the BalanceEntry is created. Every BalanceEntry is created with authorize = FALSE, and the authorize field is the only mutable part of a BalanceEntry. All other fields are as specified in the CreateBalance operation.

CreateBalance returns

  • CREATE_BALANCE_MALFORMED if the asset is invalid or the amount is non-positive
  • CREATE_BALANCE_NOT_AUTHORIZED if the asset is AUTH_REQUIRED and the source account does not have an authorized trust line
  • CREATE_BALANCE_LOW_RESERVE if the source account has insufficient native asset available (accounting for reserve and liabilities) to send baseReserve
  • CREATE_BALANCE_UNDERFUNDED if the source account has insufficient asset available (accounting for liabilities) to send amount
  • CREATE_BALANCE_SUCCESS otherwise

ClaimBalance

A BalanceEntry can only be deleted by the ClaimBalance operation. The source account of the ClaimBalance operation must match claimableBy if closeTime is before revokeTime, or must match createdBy otherwise. If the BalanceEntry is claimed by claimableBy and claimableBy does not have a trust line for the asset, then ClaimBalance will create one with limit equal to amount. If the BalanceEntry is claimed by claimableBy and authorize = TRUE, then ClaimBalance will authorize the trust line (regardless of whether it was just created).

ClaimBalance returns

  • CLAIM_BALANCE_DOES_NOT_EXIST if there is no BalanceEntry matching balanceID
  • CLAIM_BALANCE_CANNOT_CLAIM if the source account does not match the the relevant of createdBy or claimableBy, depending on the closeTime and revokeTime
  • CLAIM_BALANCE_LOW_RESERVE if the source account does not have a trust line for asset, and it does not have sufficient balance (accounting for reserve and liabilities) to create a trust line even after receiving the reserve from the BalanceEntry
  • CLAIM_BALANCE_NOT_AUTHORIZED if the source account does not have an authorized trust line (checked after ClaimBalance potentially authorizes the trust line)
  • CLAIM_BALANCE_LINE_FULL if the source account does not have sufficient available balance of native asset (accounting for liabilities) to receive reserve, or does not have sufficient available balance of asset (accounting for liabilities) to receive amount
  • CLAIM_BALANCE_SUCCESS otherwise

AuthorizeBalance

The only part of a BalanceEntry that is mutable is the authorize flag, which can be changed by the issuer of asset using AuthorizeBalance.

AuthorizeBalance returns

  • AUTHORIZE_BALANCE_DOES_NOT_EXIST if there is no BalanceEntry matching balanceID
  • AUTHORIZE_BALANCE_NOT_ISSUER if the source account is not the issuer of asset
  • AUTHORIZE_BALANCE_TRUST_NOT_REQUIRED the asset is not AUTH_REQUIRED
  • AUTHORIZE_BALANCE_CANT_REVOKE the asset is AUTH_IMMUTABLE
  • AUTHORIZE_BALANCE_SUCCESS otherwise

Design Rationale

BalanceEntry is not a sub-entry

Each BalanceEntry exists as an independent entity on the ledger. It is clear that the BalanceEntry cannot be a sub-entry of claimableBy because it is a security risk for accounts to be able to add sub-entries to other accounts. But why should the BalanceEntry be an independent entity on the ledger rather than a sub-entry of createdBy? There are three main benefits of this design:

  1. Sending accounts are not limited in the number of payments they can initiate
  2. Sending accounts can be merged even if they initiated payments that have not been claimed
  3. Receiving accounts always receive the reserve from the claimed BalanceEntry

The third benefit facilitates a desirable optimization in which a BalanceEntry can be directly converted into a TrustLine. This is useful in some asset issuance workflows where the issuer wants to pay for the trustline but does not want the recipient to spend the funds prior to creating the trustline. Of course if the base reserve were raised after the BalanceEntry was created then the reserve received would not be sufficient to pay for the trustline, but they would at least still receive the reserve. But if the BalanceEntry were a sub-entry of the sending account and the base reserve were raised such that the sending account no longer satisfies the reserve requirement then the receiving account would receive no reserve (because accounts that do not satisfy the reserve requirement cannot have their native balance decrease). While this is an unlikely edge case, it does simplify the behavior of BalanceEntry by guaranteeing that the receiving account always receives both the reserve and the amount of asset.

BalanceEntry createdBy and claimableBy are accountIDs

Each BalanceEntry can only be claimed by claimableBy or createdBy (exactly one of them depending on the ledger close time). As a consequence, it is possible for a BalanceEntry to become stranded. If claimableBy is merged and the private key is not known, then the BalanceEntry will never be claimable by the intended recipient. Similarly if createdBy is merged and the private key is not known, then the BalanceEntry will never be reclaimable by the sender.

Suppose that we try to relax this requirement in order to avoid this downside. We could instead attach two public keys to the BalanceEntry, where the sender alone knows one of the private keys and the recipient alone knows the other. Which key is currently usable would be determined by the ledger close time. The operation to claim the BalanceEntry could then specify an account A and a signature over A from the key that is currently usable. This would allow the appropriate party to claim the BalanceEntry 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 BalanceEntry with a recipient public key whose private key is known only to some other party. That party would then control the funds in the BalanceEntry 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 BalanceEntry to be claimable into any account would significantly weaken the strength of authorization restrictions.

BalanceEntry facilitates authorization

When issuing an authorization required asset to an account that does not exist, there is a chicken-and-egg problem. The account does not exist and as such does not have a trustline for the asset, so the issuer cannot authorize it. But the issuer cannot send the funds without the existence of an authorized trustline. This results in additional communication, either off-chain or on-chain. This proposal seeks to simplify the situation. When a BalanceEntry is claimed by claimableBy, a trustline for the asset will be implicitly created if one does not exist. If the authorize flag is also set, then the trustline will also be authorized regardless of whether it was implicitly created. Because the flag can only be set by the issuer and the BalanceEntry can only be claimed by claimableBy, this allows the issuer to effectively "pre-authorize". But if the issuer is using this functionality, then the issuer must be cognizant about clearing the authorize flag for any BalanceEntry paid to account A if they revoke authorization from A.

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 effectiveness of base reserve changes, because a BalanceEntry that has insufficient reserve is still usable.

Test Cases

None yet.

Implementation

None yet.