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
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 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.
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 as a
new type of transaction with corresponding envelope type ENVELOPE_TYPE_FEE_BUMP
.
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;
};
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()
andOuter(F) = F.v0()
ifF.type() == ENVELOPE_TYPE_TX_V0
Inner(F) = F.feeBump().innerTx.v0()
andOuter(F) = F.feeBump()
ifF.type() == ENVELOPE_TYPE_FEE_BUMP
The slot for a transaction envelope F is the tuple
(Inner(F).tx.sourceAccount, Inner(F).tx.seqNum)
.
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.
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 exceedsOuter(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)
.
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.
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
.
This proposal introduces several new possibilities for TransactionResultCode
:
txFEE_BUMPED
indicates that aFeeBumpTransaction
was applied (this will be returned even if theFeeBumpTransaction
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 thatDecoratedTransactionV0
orDecoratedFeeBumpTransaction
hadsignatures
that were not properly sortedtxNOT_SUPPORTED
indicates that theTransactionEnvelope
is not supported in the current protocol version. As of this proposal, this will only occur if aTransactionEnvelope
withtype == ENVELOPE_TYPE_FEE_BUMP
is submitted before the version upgrade occurstxINNER_TX_INVALID
indicates that aFeeBumpTransaction
was submitted but theinnerTx
was invalid. The result will contain aInnerTransactionResult
, which can contain any result other thantxFEE_BUMPED
andtxINNER_TX_INVALID
. This result can only be returned during validation
FeeBumpTransaction
does not contain an explicit sequence number because it
relies on the sequence number of the inner transaction for replay prevention.
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.
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.
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.
None yet.
None yet.
None yet.