CAP: 0005
Title: Throttling and transaction pricing improvements
Author: Nicolas Barry
Status: Final
Created: 2018-Oct-05
Discussion: https://github.com/stellar/stellar-protocol/issues/75
https://github.com/stellar/stellar-protocol/issues/133
Protocol version: 11
Combined set of changes that rationalize how we throttle the network, and also makes it easier for clients to craft transactions that will make it into a ledger even when network fees are changing rapidly.
Goal of the updated design is to maximize the number of operations included in a candidate transaction set, while preserving fairness.
Note that there are two mechanism at play here: one that is implemented to construct candidate values by validators which happens outside of consensus, and the other mechanism that is used to pick between two candidate sets during consensus.
Fairness in how transactions are included in a candidate set is equivalent to designing a sorting scheme for the list of transactions before trimming (typically all accumulated transactions on the validator) such that:
- accounts have the same odds of having their transactions included, regardless of the distribution of transactions per account and regardless of how many transactions are picked from the sorted set.
- transactions with a higher per operation fee have a strict advantage over lower fee transactions.
Fairness is also achieved by attempting to reduce the fee charged to accounts.
This is achieved by associating a value baseFee
to each transaction set
(greater than ledgerHeader.baseFee
), and using that base fee to compute the actual fee charged.
With this change, tx.fee
becomes an upper bound instead of being the actual
fee deducted from the source account.
Right now the network configuration that governs the maximum throughput of the
network is ledgerHeader.maxTxSetSize
, that controls the maximum number of
transactions that can be included in a transaction set; yet a good approximation
of work (reflected in how fees are computed), is the total number of operations
included in a transaction set.
As any transaction can contain up to 100 operations, this causes the network
setting to be overly conservative (as it has to assume the worst case situation
where all transactions would be 100 operations).
Another motivation for changing how fees are computed is that right now the fee charged for a given transaction is equal to the amount specified when the transaction was crafted.
When surge pricing occurs, or simply if ledgerHeader.baseFee
is raised, this
creates problems:
- for the typical user, this leads to transactions rejected by validators if the specified fee is too small.
- for pre-signed transactions (smart contracts), the fee has to be specified with a value that exceeds the future fee required to be included in a transaction set.
- in both cases the model can lead to excessive fees charged to users if the specified fee is bigger than needed.
In the following specification, we'll use StellarValue v
with its associated
TransactionSet txSet
and LedgerHeader ledgerHeader
.
No changes in XDR
For ledgers with a protocolVersion supporting CAP0005, this setting controls the maximum number of operations allowed in a transaction set (currently it controls the number of transactions in the transaction set).
- pick a random number S
- for each account, construct a queue of transactions for that account,
sorted by sequence number in ascending order
- implementation note: there might be an opportunity to consolidate with the transaction holding tank.
- construct a heap of those queues, sorted by:
- the fee rate (descending) of the first transaction in each queue (so that the queue whose first transaction has the highest fee rate is on top of the heap)
- first transaction hash Xored with S (ascending)
nb_operations_to_add = maxTxSetSize
- while (
nb_operations_to_add > 0 && !heap.empty()
)- queue = heap.pop()
- if nb_operations(first transaction "tx" from queue) <= nb_operations_to_add
- push tx into txSet
- nb_operations_to_add = nb_operations_to_add - nb_operations(tx)
- pop tx from queue
- if queue non empty, push it into the heap
- if
count_ops(txSet) > max(0, ledgerHeader.maxTxSetSize-100)
- surge pricing in effect
NB: if ledgerHeader.ledgerVersion < CAP_0005.protocolVersion
,
- step 4 becomes
nb_operations_to_add = maxTxSetSize*100
- step 5 defaults
nb_operations(tx)
to100
The effective base fee for protocol supporting CAP_0005 is defined as follows:
- if
nb_operations(txSet) <= max(0, ledgerHeader.maxTxSetSize-100)
- surge pricing not in effect
- effectiveBaseFee =
ledgerHeader.baseFee
- effectiveBaseFee =
- surge pricing not in effect
- else, we use the smallest possible base fee for that transaction set
- effectiveBaseFee =
min(baseFeeForTx(for(tx in txSet)))
- effectiveBaseFee =
Where baseFeeForTx
is defined as tx.fee / tx.ops.length()
rounded up.
NB: Surge pricing is triggered as soon as we can't assume that a transaction was not included in the transaction set given the limit ledgerHeader.maxTxSetSize
, which comes to ledgerHeader.maxTxSetSize-100
.
The computation of fee = computedFee(tx, effectiveBaseFee)
is done as:
- if
ledgerHeader.ledgerVersion < CAP_0005.protocolVersion
- fee =
tx.fee
- fee =
- else
- fee =
min(tx.fee, effectiveBaseFee*tx.ops.length())
- fee =
From a fee point of view, the only requirement is that each source account has
a balance sufficient to cover the combined computed fees of that account's
transactions included in txSet
if ledgerHeader.ledgerVersion < CAP_0005.protocolVersion
we enforce that txSet.length() <= maxTxSetSize
.
Otherwise we enforce that nb_operations(txSet) <= maxTxSetSize
The ballot protocol requires a function to combine candidate values produced by the nomination protocol.
For older versions of the protocol, we do not change this function, ie we keep the transaction set with the highest number of transactions, and break ties by comparing the hash of transaction sets (XORed to pseudo-randomize).
For newer versions of the protocol, we'll be following a similar scheme by picking the highest value sorted in order by:
- highest number of operations
- highest total fee (sum of fees charged)
- highest txset hash (XORed by the hash of all value as before)
Implementing this proposal should greatly increase the chances of transactions being processed by the network even when fees vary between ledgers.
More improvements can be made, in particular for very long lived pre-signed transactions (where there is no rational way of putting a high bound on fees), or for more complex client side fee control strategies.
This proposal assumes that there is no inherent incentive in validators profiting from the rise of fees which probably stays true for the foreseeable future.
The change is backward compatible with existing transactions and will be transparent to clients.
The only breaking change is that accounts may have a higher balance than
expected if fee
is higher than the minimum fee.
Smart contracts typically don't rely on exact balances and use mergeAccountOp
as part of their cleanup that doesn't depend on having a predetermined balance.
The exact value generated by surge pricing doesn't need to match even when the network is still operating on an older version of the protocol.
As such, there is no concern to try to preserve the existing behavior.
The behavior is entirely driven by the value of the protocol version, the change is therefore backward compatible.
After the network upgrades to the new protocol version the value of
maxTxSetSize
will be smaller than desired (as the raw value will not
represent transactions but operations instead); A subsequent vote by validators
will be need to be coordinated in order to adjust it to a new desired value.
Practically speaking the disruption should be minimal:
- Most transactions are single operation transactions and won't be effected.
- Disruption will be mostly for transactions with more operations than
maxTxSetSize
. As of this writing, this network parameter is set to50
on the public network, which means that transactions with more than50
operations will not be accepted by the network between the two upgrades. - Validators can coordinate a "double upgrade", where both the protocol version
and the
maxTxSetSize
field gets updated.
TBD
TBD