CAP: 0015
Title: Fee-Bump Transactions
Author: Jonathan Jove, OrbitLens <[email protected]>
Status: Final
Created: 2018-11-26
Updated: 2019-12-03
Discussion: https://groups.google.com/forum/#!topic/stellar-dev/ckzBRlfr2VU
Protocol version: 13
This proposal introduces fee-bump transactions, which allow an arbitrary account to pay the fee for a transaction.
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.
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.
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.
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;
};
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.
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.
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 returntxINSUFFICIENT_FEE
-
The fee rate for
E
is at least the fee rate ofF
, otherwise returntxINSUFFICIENT_FEE
-
E.feeBump().tx.feeSource
exists, otherwise returntxNO_ACCOUNT
-
E.feeBump().signatures
contains signatures forE.feeBump().tx
with total weight at least equal to the low threshold forE.feeBump().tx.feeSource
(pre-authorized transaction hashes are implicitly included here), otherwise returntxBAD_AUTH
-
E.feeBump().tx.feeSource
has sufficient available native balance (see CAP-0003) to payE.feeBump().tx.fee
in addition to fees that will be paid by this account for other transactions, otherwise returntxINSUFFICIENT_BALANCE
-
F
is valid, except thatF.v1().tx.fee
may be less than the minimum fee forF
, but must be non-negativeF.v1().tx.sourceAccount
may not have sufficient available native balance
otherwise return
txINNER_FAILED
with the inner result set to the result of validatingF
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.
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
.
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.
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).
FeeBumpTransaction
does not contain an explicit sequence number because it
relies on the sequence number of the inner transaction for replay prevention.
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.
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.
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.
None yet.
None yet.
This proposal was implemented in stellar/stellar-core#2419.