Skip to content

Latest commit

 

History

History
391 lines (341 loc) · 15 KB

cap-0015.md

File metadata and controls

391 lines (341 loc) · 15 KB

Preamble

CAP: 0015
Title: Fee-Bump Transactions
Author: Jonathan Jove, OrbitLens <orbit.lens@gmail.com>
Status: Final
Created: 2018-11-26
Updated: 2019-12-03
Discussion: https://groups.google.com/forum/#!topic/stellar-dev/ckzBRlfr2VU
Protocol version: 13

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 12. 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 12, 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.

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 with corresponding envelope type ENVELOPE_TYPE_TX_FEE_BUMP as a new type of transaction. 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.

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_TX_FEE_BUMP = 5
};

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

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

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

struct FeeBumpTransaction
{
    AccountID feeSource;
    int64 fee;
    union switch (EnvelopeType type)
    {
    case ENVELOPE_TYPE_TX:
        TransactionV1Envelope v1;
    } innerTx;
    union switch (int v) {
    case 0:
        void;
    } ext;
};

struct FeeBumpTransactionEnvelope
{
    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:
    TransactionV0Envelope v0;
case ENVELOPE_TYPE_TX:
    TransactionV1Envelope v1;
case ENVELOPE_TYPE_TX_FEE_BUMP:
    FeeBumpTransactionEnvelope 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_TX_FEE_BUMP:
        FeeBumpTransaction feeBump;
    }
    taggedTransaction;
};

The new transaction result XDR types are:

enum TransactionResultCode
{
    txFEE_BUMP_INNER_SUCCESS = 1, // fee bump inner transaction succeeded
    // .... txSUCCESS, ..., txINTERNAL_ERROR unchanged ....
    txNOT_SUPPORTED = -12,  // transaction type not supported
    txFEE_BUMP_INNER_FAILED = -13 // fee bump inner transaction failed
};

struct InnerTransactionResult
{
    int64 feeCharged;

    union switch (TransactionResultCode code)
    {
    // txFEE_BUMP_INNER_SUCCESS 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_SUPPORTED:
        // txFEE_BUMP_INNER_FAILED is not included
        void;
    }
    result;

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

struct InnerTransactionResultPair
{
    Hash transactionHash;          // hash of the inner transaction
    InnerTransactionResult result; // result for the inner transaction
};

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

    union switch (TransactionResultCode code)
    {
    case txFEE_BUMP_INNER_SUCCESS:
    case txFEE_BUMP_INNER_FAILED:
        InnerTransactionResultPair innerResultPair;
    case txSUCCESS:
    case txFAILED:
        OperationResult results<>;
    default:
        void;
    }
    result;

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

Semantics

How does the TransactionEnvelope transformation work?

In order to create multiple types of transaction envelopes, we needed to perform a clever transformation of the XDR. We split the discriminant off Transaction.sourceAccount leaving a raw ed25519 public key (this type is TransactionV0), then used the discriminant as the discriminant for the TransactionEnvelope union. We then added a new type of transaction envelope, which just contains a Transaction, to support new account types should we add them in the future. The third type of transaction is the fee-bump transaction, which can wrap a new-style transaction envelope of type ENVELOPE_TYPE_TX.

Fee Rate

A fee-bump transaction has an effective number of operations equal to one plus the number of operations in the inner transaction. Correspondingly, the minimum fee for the fee-bump transaction is one base fee more than the minimum fee for the inner transaction. Similarly, the fee rate (see CAP-0005) is normalized by one plus the number of operations in the inner transaction rather than the number of operations in the inner transaction alone.

Validity

Prior to the protocol version in which this proposal is implemented, only a TransactionEnvelope of type ENVELOPE_TYPE_TX_V0 can be valid whereas a TransactionEnvelope of any other type will be invalid with result txNOT_SUPPORTED. Starting in the protocol version in which this proposal is implemented, only a TransactionEnvelope of type ENVELOPE_TYPE_TX or ENVELOPE_TYPE_TX_FEE_BUMP can be valid whereas a TransactionEnvelope of any other type will be invalid with result txNOT_SUPPORTED. Because ENVELOPE_TYPE_TX_V0 and ENVELOPE_TYPE_TX require the same signatures, TransactionEnvelope of type ENVELOPE_TYPE_TX_V0 can simply be converted to ENVELOPE_TYPE_TX.

Validity requirements for a TransactionEnvelope of type ENVELOPE_TYPE_TX are identical to the existing validity requirements for a TransactionEnvelope of type ENVELOPE_TYPE_TX_V0.

To validate a TransactionEnvelope E of type ENVELOPE_TYPE_TX_FEE_BUMP with inner transaction envelope F = E.feeBump().tx.innerTx, check that

  • E.feeBump().tx.fee is at least the minimum fee, otherwise return txINSUFFICIENT_FEE

  • The fee rate for E is at least the fee rate of F, otherwise return txINSUFFICIENT_FEE

  • E.feeBump().tx.feeSource exists, otherwise return txNO_ACCOUNT

  • E.feeBump().signatures contains signatures for E.feeBump().tx with total weight at least equal to the low threshold for E.feeBump().tx.feeSource (pre-authorized transaction hashes are implicitly included here), otherwise return txBAD_AUTH

  • E.feeBump().tx.feeSource has sufficient available native balance (see CAP-0003) to pay E.feeBump().tx.fee in addition to fees that will be paid by this account for other transactions, otherwise return txINSUFFICIENT_BALANCE

  • F is valid, except that

    • F.v1().tx.fee may be less than the minimum fee for F, but must be non-negative
    • F.v1().tx.sourceAccount may not have sufficient available native balance

    otherwise return txINNER_FAILED with the inner result set to the result of validating F

Replace-by-Fee

If a node receives a valid TransactionEnvelope E' of type ENVELOPE_TYPE_TX_FEE_BUMP with source account A and sequence number N when it already has a TransactionEnvelope E with source account A and sequence number N in the transaction queue, then it should replace E with E' if and only if the fee rate for E' is at least 10 times the fee rate for E. It should be emphasized that source account in this section refers specifically to the source account of the sequence number of the transaction, although these transactions may still have different fee source accounts.

Surge Pricing

The logic for surge pricing is unchanged. The only new consideration is that a TransactionEnvelope E of type ENVELOPE_TYPE_TX_FEE_BUMP should be included in the queue for E.feeBump().tx.innerTx.v1().sourceAccount rather than the queue for E.feeBump().tx.feeSource.

Application and Results

Even if the outer transaction of a fee-bump is invalid during application, we will still apply the inner transaction. Specifically, the behavior is:

  • Remove used one-time signers for the outer transaction
  • Apply the inner transaction (as if there were no outer transaction)

If the inner transaction succeeds, then the result is txFEE_BUMP_INNER_SUCCESS and the innerResultPair is what would have been produced by the inner transaction in the absence of the outer transaction. Analogously, if the inner transaction fails, then the result is txFEE_BUMP_INNER_FAILED and the innerResultPair is what would have been produced by the inner transaction in the absence of the outer transaction. No other results are possible.

Rationale

Make ENVELOPE_TYPE_TX_V0 Invalid

This proposal uses the same signatures for TransactionEnvelope of type ENVELOPE_TYPE_TX_V0 and ENVELOPE_TYPE_TX. This makes it possible to convert a TransactionEnvelope of type ENVELOPE_TYPE_TX_V0 into a TransactionEnvelope of type ENVELOPE_TYPE_TX without needing new signatures. The main advantage of this is it allows us to make ENVELOPE_TYPE_TX_V0 invalid and only support ENVELOPE_TYPE_TX in FeeBumpTransaction. Furthermore, making ENVELOPE_TYPE_TX_V0 invalid simplifies the possibility of having a "canonical" or "normalized" XDR representation of a TransactionEnvelope (this concept has arisen during the discussion of some CAPs in the past).

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

A fee-bump transaction has an effective number of operations strictly greater than the number of operations in the inner transaction because it requires more work for the network to process a fee-bump transaction than the inner transaction alone.

The semantics specify that a fee-bump transaction is invalid if the fee rate of the outer transaction does not exceed the fee rate 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'. So they use a FeeBumpTransaction with the outer fee set to F'. But if the outer fee rate is less than the inner fee rate, then in any case where the fee-bump could be included in the transaction set the inner transaction could also have been included. This completely defeats the purpose of submitting the FeeBumpTransaction with fee F' because a malicious adversary could always cause you to pay fee F. Therefore we prohibit this possibility entirely.

Application and Results

The only purpose of a fee-bump transaction is to bid a higher fee for the inner transaction to either make the transaction valid in the event that the fee rate is less than the base fee, or to accelerate inclusion of the transaction in the event of surge pricing. In either case, the fee-bump has fulfilled its role once it has been included in the transaction set. The source account for each transaction is charged the fee before any transactions are actually applied, so all that remains when applying a fee-bump transaction is to remove used one-time signers and to apply the inner transaction.

There are several reasons that the inner transaction must be applied regardless of the validity of the outer transaction at apply time. The most important reason is replay protection: as noted above, the sequence number of the inner transaction provides replay protection for the fee-bump transaction. If the inner transaction were not applied when the outer transaction became invalid at apply time, then the sequence number would not be consumed in that case. This would allow the fee-bump transaction to be played again if the outer transaction were to become valid again.

Given the above, there is no reason to return the result of the fee-bump transaction separately from the result of the inner transaction.

Backwards Incompatibilities

All downstream systems which submit transactions to stellar-core will need to transform TransactionEnvelope of type ENVELOPE_TYPE_TX_V0 to type ENVELOPE_TYPE_TX before submission. As a temporary bridge, stellar-core will perform this transformation for transactions submitted to the HTTP endpoint. But this bridge will not work for the newly introduced FeeBumpTransactions, which will need explicit support for transaction normalization in downstream systems.

It follows from the above that downstream systems that submit transactions will continue to function, but downstream systems that process historical transactions will need to be updated with the new XDR.

Security Concerns

None yet.

Test Cases

None yet.

Implementation

This proposal was implemented in stellar/stellar-core#2419.