Skip to content

Latest commit

 

History

History
345 lines (296 loc) · 12.4 KB

cap-0015.md

File metadata and controls

345 lines (296 loc) · 12.4 KB

Preamble

CAP: 0015
Title: Fee-Bump Transactions and the Normalized TransactionEnvelope Format
Author: Jonathan Jove, OrbitLens <orbit.lens@gmail.com>
Status: Draft
Created: 2018-11-26
Updated: 2019-09-30
Discussion: https://groups.google.com/forum/#!topic/stellar-dev/ckzBRlfr2VU
Protocol version: TBD

Simple Summary

This proposal introduces fee-bump transactions, which allow an arbitrary account to pay the fee for a transaction.

Motivation

If a transaction has insufficient fee, for example due to either surge pricing or an increase in the baseFee, that transaction will not be included in a transaction set. If the transaction is only signed by the party that wants to execute it then it is possible to simply craft a new transaction with higher fee, sign it, and submit it. But there are many circumstances where this is not possible, such as when pre-signed and pre-authorized transactions are involved. In these cases, it is possible that a high-value transaction (such as the release of escrowed funds or the settlement of a payment channel) cannot execute as of protocol version 11. Fee-bump transactions will resolve this issue by enabling anyone to pay the fee for an already existing transaction.

The mechanism described here will also facilitate another usage: when applications (such as games) are willing to pay user fees. As of protocol version 11, this could be resolved in two ways. In the first approach, funds could be sent directly to the users' account but there is no guarantee that the user will spend those funds on fees and there is no way to recover unspent funds. In the second approach, one or more accounts controlled by the application could be used as the source account for user transactions but this leads to sequence number management issues.

Fee-bump transactions as described in this proposal will enable any account to pay the fee for an existing transaction without the need to re-sign the existing transaction or manage sequence numbers.

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 make it easy for developers of Stellar projects to create highly usable products.

Abstract

TransactionEnvelope is transformed from an XDR struct to an XDR union while preserving binary compatibility. FeeBumpTransaction is introduced as a new type of transaction with corresponding envelope type ENVELOPE_TYPE_FEE_BUMP.

Specification

XDR

The new transaction, transaction envelope, and related XDR types are:

enum EnvelopeType
{
    ENVELOPE_TYPE_TX_V0 = 0,
    ENVELOPE_TYPE_SCP = 1,
    ENVELOPE_TYPE_TX = 2,
    ENVELOPE_TYPE_AUTH = 3,
    ENVELOPE_TYPE_SCPVALUE = 4,
    ENVELOPE_TYPE_FEE_BUMP = 5
};

struct TransactionV0
{
    uint256 sourceAccountEd25519;
    uint32 fee;
    SequenceNumber seqNum;
    TimeBounds* timeBounds;
    Memo memo;
    Operation operations<100>;
    union switch (int v) {
    case 0:
        void;
    } ext;
};

struct DecoratedTransactionV0
{
    TransactionV0 tx;
    /* Each decorated signature is a signature over the SHA256 hash of
     * a TransactionSignaturePayload */
    DecoratedSignature signatures<20>;
};

struct FeeBumpTransaction
{
    AccountID feeSource;
    uint64 fee;
    TimeBounds *timeBounds;
    union switch (EnvelopeType type)
    {
    case ENVELOPE_TYPE_TX_V0:
        DecoratedTransactionV0 v0;
    } innerTx;
};

struct DecoratedFeeBumpTransaction
{
    FeeBumpTransaction tx;
    /* Each decorated signature is a signature over the SHA256 hash of
     * a TransactionSignaturePayload */
    DecoratedSignature signatures<20>;
};

union TransactionEnvelope switch (EnvelopeType type) {
case ENVELOPE_TYPE_TX_V0:
    DecoratedTransactionV0 v0;
case ENVELOPE_TYPE_FEE_BUMP:
    DecoratedFeeBumpTransaction feeBump;
};

struct TransactionSignaturePayload
{
    Hash networkId;
    union switch (EnvelopeType type)
    {
    // Backwards Compatibility: Use ENVELOPE_TYPE_TX to sign ENVELOPE_TYPE_TX_V0
    case ENVELOPE_TYPE_TX:
        Transaction tx;
    case ENVELOPE_TYPE_FEE_BUMP:
        FeeBumpTransaction feeBump;
    }
    taggedTransaction;
};

The new transaction result XDR types are:

enum TransactionResultCode
{
    txFEE_BUMPED = 1,
    // .... txSUCCESS, ..., txINTERNAL_ERROR unchanged ....
    txNOT_NORMALIZED = -12,      // signatures are not sorted
    txNOT_SUPPORTED = -13,       // transaction type not supported
    txINNER_TX_INVALID = -14     // fee bump inner transaction invalid, never
                                 // returned during apply
};

struct InnerTransactionResult
{
    int64 feeCharged;

    union switch (TransactionResultCode code)
    {
    // txFEE_BUMPED is not included
    case txSUCCESS:
    case txFAILED:
        OperationResult results<>;
    case txTOO_EARLY:
    case txTOO_LATE:
    case txMISSING_OPERATION:
    case txBAD_SEQ:
    case txBAD_AUTH:
    case txINSUFFICIENT_BALANCE:
    case txNO_ACCOUNT:
    case txINSUFFICIENT_FEE:
    case txBAD_AUTH_EXTRA:
    case txINTERNAL_ERROR:
    case txNOT_NORMALIZED:
    case txNOT_SUPPORTED:
    // txINNER_TX_INVALID is not included
        void;
    }
    result;

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

struct TransactionResult
{
    int64 feeCharged; // actual fee charged for the transaction

    union switch (TransactionResultCode code)
    {
    case txSUCCESS:
    case txFAILED:
        OperationResult results<>;
    case txINNER_TX_INVALID:
        InnerTransactionResult innerResult;
    default:
        void;
    }
    result;

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

Semantics

For this subsection, let E be a transaction envelope with E.type() == ENVELOPE_TYPE_FEE_BUMP.

If F is a transaction envelope then

  • Inner(F) = F.v0() and Outer(F) = F.v0() if F.type() == ENVELOPE_TYPE_TX_V0
  • Inner(F) = F.feeBump().innerTx.v0() and Outer(F) = F.feeBump() if F.type() == ENVELOPE_TYPE_FEE_BUMP

The slot for a transaction envelope F is the tuple (Inner(F).tx.sourceAccount, Inner(F).tx.seqNum).

Normalization

We say that a transaction envelope F is normalized if Outer(F).signatures and Inner(F).signatures are both sorted in ascending order according to the relation: A < B if A.hint < B.hint || (A.hint == B.hint && A.signature < B.signature) for any decorated signatures A and B. E is invalid if it is not normalized.

Validity

Inner(E).fee must be non-negative. It does not need to exceed the minimum fee because the fee is specified by Outer(E).fee. There are no requirements on the balance of Inner(E).sourceAccount because the fee will be paid by Outer(E).feeSource. Other than these two exceptions, Inner(E) must be valid independent of E.

Outer(E).fee must exceed the minimum fee of Inner(E) by at least one base fee. Furthermore, the effective fee of Outer(E) must exceed the effective fee of Inner(E). Specifically, Outer(E).fee / [Inner(E).operations.size() + 1] must exceed Inner(E).fee / Inner(e).operations.size(). Outer(E).feeSource needs sufficient available balance (accounting for reserve and native selling liabilities) to pay Outer(E).fee.

If Outer(E).timeBounds is specified then

  • E is too early if Outer(E).timeBounds.minTime exceeds the ledger close time
  • E is too late if Outer(E).timeBounds.maxTime != 0 and the ledger close time exceeds Outer(E).timeBounds.maxTime

Outer(E).signatures must contain signatures satisfying the low threshold of the feeSource account. To be explicit, signatures in Outer(E).signatures only provide signing weight to E whereas signatures in Inner(E).signatures only provide signing weight to Inner(E).

Surge Pricing

The effective fee of E is equal to Outer(E).fee / [Inner(E).tx.operations.size() + 1]. This would be the effective fee of Inner(E) if it had one additional operation.

When constructing a transaction set, E will be in the transaction queue for Inner(E).sourceAccount. This ensures that sequence number ordering will be respected.

If E1, ..., En is the set of valid transaction envelopes with a given slot, then when any of them has been included in the transaction set the rest must be removed from the transaction queue. They will be placed into a separate priority queue that tracks all transaction envelopes that no longer have any impact on sequence number ordering. This makes it possible to include transactions with both higher sequence numbers and higher fees before adding additional transactions with lower sequence numbers and lower fees.

If E and Inner(E) are both in the transaction queue and E is included in the transaction set, then Inner(E) will necessarily be included as well. This makes sense because it costs no additional operations to add Inner(E), but will add additional fees.

When adding a new transaction envelope to the transaction set, it will be the transaction envelope with the highest fee among the tops of the two priority queues.

Applying

If E1, ..., En is the set of transaction envelopes with a given slot that are in the transaction set, then Inner(E1) == ... == Inner(En). Either Ek = Inner(E1) for exactly one k in {1, ..., n}, or it should be added to the transaction set as E(n+1). In either case, Inner(E1) will be applied exactly once and all other transaction envelopes with the same slot will return txFEE_BUMPED.

Transaction Results

This proposal introduces several new possibilities for TransactionResultCode:

  • txFEE_BUMPED indicates that a FeeBumpTransaction was applied (this will be returned even if the FeeBumpTransaction is no longer valid, for example if it were not properly authorized, by the time it is reached in the transaction apply order)
  • txNOT_NORMALIZED indicates that DecoratedTransactionV0 or DecoratedFeeBumpTransaction had signatures that were not properly sorted
  • txNOT_SUPPORTED indicates that the TransactionEnvelope is not supported in the current protocol version. As of this proposal, this will only occur if a TransactionEnvelope with type == ENVELOPE_TYPE_FEE_BUMP is submitted before the version upgrade occurs
  • txINNER_TX_INVALID indicates that a FeeBumpTransaction was submitted but the innerTx was invalid. The result will contain a InnerTransactionResult, which can contain any result other than txFEE_BUMPED and txINNER_TX_INVALID. This result can only be returned during validation

Rationale

Replay Prevention

FeeBumpTransaction does not contain an explicit sequence number because it relies on the sequence number of the inner transaction for replay prevention.

Fees

The semantics specify that a fee-bump transaction is invalid if the effective fee of the outer transaction does not exceed the effective fee of the inner transaction. This restriction is designed to prevent an obvious misunderstanding of the semantics of FeeBumpTransaction. Suppose someone has a signed transaction with fee F, but they actually do not want to pay a fee greater than F' < F. Then they use a FeeBumpTransaction with the outer fee set to F'. But if the FeeBumpTransaction can be included in the transaction set, then the inner transaction also could be included in the transaction set because it has a higher fee. This completely defeats the purpose of submitting the FeeBumpTransaction with fee F' < F, so we prohibit this possibility entirely.

Normalization

As of protocol 11, it is not possible for a transaction set to contain multiple transactions with the same slot. This will change with the introduction of fee bump transactions. Still no single transaction should be included in a transaction set more than once and only one transaction with a given slot can be applied. In order to resolve these issues with minimal burden on the implementor, we propose that a transaction envelope is invalid if it is not normalized.

Backwards Incompatibilities

All downstream systems which submit transactions to stellar-core will need to normalize transactions before submission. As a temporary bridge, stellar-core will normalize submitted transactions. But this bridge will not work for the newly introduced FeeBumpTransactions, which will need explicit support for transaction normalization in downstream systems.

Security Concerns

None yet.

Test Cases

None yet.

Implementation

None yet.