Implement fee estimation algorithm for dynamic fee system

Dear community,

With this post, I would like to start the discussion and gather your feedback regarding the “Implement fee estimation algorithm for the dynamic fee system” LIP below.

The LIP addresses one of the objectives in the next roadmap phase “Network Economics”.

LIP: LIP-implement_fee_estimation_algorithm_for_dynamic_fee_system
Title: Implement fee estimation algorithm for dynamic fee system
Author: Iker Alustiza <iker@lightcurve.io>
Type: Standards Track
Created: -
Updated: -
Requires: LIP-replace_static_fees

Abstract

This LIP proposes a fee estimation algorithm that suggests a fee depending on the priority of the transaction for the user and the blockchain’s recent history. The algorithm is based on an Exponential Moving Average (EMA). It is implemented in a node of the Lisk network and the wallet queries its output to get the transaction fee estimation for the user.

Copyright

This LIP is licensed under the Creative Commons Zero 1.0 Universal.

Motivation

With the proposed dynamic fee system for Lisk (refer to the Replace static fee LIP for the complete proposal), there will be a minimum fee required by the protocol, and the users will be able to spend any fee between this minimum fee and the top limit permitted by their account balance. This has two implications for the users:

  1. The users have to know the exact minimum fee for their transactions (depending on the type and size) so that they can assign a valid fee for their transactions.

  2. The users need certain guidance about what fee to spend in their transactions depending on their priority. This way the users minimize their transactions’ processing cost. This entails benefits for the Lisk network in terms of usability. Arguably, one can say that a good guidance for the user in this respect allows to fully attain the advantages of a dynamic fee system.

In this proposal we assume that 1. is already given and accessible for the user (in any Lisk wallet or other third party wallets). In fact, it is straightforward to implement given the minimum required fee per byte and the type and size of the transaction. However, 2. implies a deeper technical study, mathematical modeling and definition of this guidance for the user.

Rationale

We propose a fee estimation algorithm to leverage the features of the dynamic fee system and attain its advantages for the Lisk network. This fee estimation algorithm provides an estimation of what transaction fee should be used depending on the priority of the transaction for the user and the blockchain’s recent history (i.e., the guidance for the user). To achieve this objective, the fee estimation algorithm proposed in this document has the following properties:

  • The output is correlated to the information in the blockchain, i.e., the fee spent by the confirmed transactions.
  • The algorithm considers as much data in the past as possible for better accuracy.
  • The newer this data is, the greater its importance is in the estimation.
  • If the last blocks have space for additional transactions, the minimum required fee is recommended.
  • It provides different estimates depending on the priority the users give to their transactions.
  • The output cannot be higher than the static fees in the current protocol.

On top of all these properties, the user should understand that the output of the algorithm (the fee estimation) is only a recommendation or a suggestion. It is just a guidance to help the user, not a rule given by the protocol.

Taking the properties above into account, the proposed fee estimation algorithm is based on an Exponential Moving Average (EMA), also referred to as exponential smoothing in the literature. With the EMA, the data considered can theoretically expand infinitely in the past while its weight decreases exponentially with time. In signal processing literature (where the technique is addressed as exponential smoothing), several studies are available about how to tune the weight of the data depending on its age.

For the mentioned idea of transaction priority, we assume that the economic interest of the delegates is to choose the transactions with highest fees per byte to be included first into blocks. This way, the higher the fees per byte of a transaction are, the higher its priority is (faster inclusion in the blockchain)[1]. For this proposal, we consider three different transaction priorities with respect to the fee. The notion behind these three transaction priorities is:

  • Low priority transaction fee: Fee estimation considering the transactions with the lowest fees per byte in the past blocks.
  • Medium priority transaction fee: Fee estimation considering the transactions with approximately average fees per byte in the last blocks.
  • High priority transaction fee: Fee estimation considering the transactions with highest fees per byte in the past blocks.

With the previous rationale in mind, it is important to note that the algorithm is conceptually simple and does not attempt to predict future conditions, but assumes that these conditions are correlated to the recent past. Mathematically, it considers some recent history of transactions in the blockchain and outputs a certain fee per byte such that in this recent history a considerable amount of transactions with that fee (or higher) were confirmed in the blockchain.

This proposal takes inspiration from estimateSmartFee function in Bitcoin Core in terms of philosophy and objectives. However the proposed approach defines a different mathematical model and fee estimation methodology.

[1] Note that when we say here “higher fees per byte”, we are referring to the fees paid on top of the minimum fee of a transaction, namely feePriority in the Replace static fee LIP.

Mathematical description

As mentioned above, the algorithm internally is modelled as an EMA, which in general can be described as the following recursive formula:

feeEst0 = offset

feeEsth = α feeh + (1- α) feeEsth-1   for h > 0

where:

  • α is the smoothing factor or exponential decay. This parameter, between 0 and 1, defines how fast the weight of past information decays in the EMA. For example, if α ≈ 1, then feeh is given a large weight, while all other weights are almost zero. On the contrary, if α is close to 0 then older estimates are given a significant weight for the current estimation. Given a number d between 0 and 1, and a positive integer B, such that the weight of the information B blocks in the past has a decay of d, then α is determined by:

    (1-α)B = 1 - d

  • offset is the parameter to signify the past fees prior the initial estimate.

  • feeEsth is the estimation of the fee computed at height h.

  • feeh is the fee per byte derived from the block at height h. It is given in terms of the fee per byte paid on top of the minimum fee for a transaction. In particular, it is based on the feePriority concept defined in the Replace static fee LIP as:

    feePriority(trs) = (trs.fee - trs.minFee) / sizeof(trs)

    where sizeof(trs) is the size in bytes of the transaction trs, trs.minFee is the minimum fee required for trs and trs.fee is the fee of trs. Refer to the Replace static fee LIP for the definition of trs.minFee.

Note that, in this proposal, offset, feeEsth and feeh are given in Beddows per byte. In the Lisk protocol, every amount of LSK is given in terms of Beddows (10-8 LSK).

Specification

The fee estimation algorithm is implemented in a node of the Lisk network, where the values of the estimation for every new block are calculated and kept. Locally, a wallet (i.e., Lisk Hub or Lisk Mobile) can then query a node for a fee estimation and output the suggested transaction fee to the user. Other alternative implementation considered initially can be found in Appendix A. In the following, we present the specification details:

At the node

At the node, two functions implement the fee estimation, i.e. getEstimateFeeByte and EMAcalc. The first function returns the fee estimation every time it is called via the API. It is defined as follows:

FeeEstPerByte = getEstimateFeeByte(EMAoutput)

where FeeEstPerByte and EMAoutput are objects containing the following variables:

FeeEstPerByte:{
  Low,
  Med,
  High,
}
EMAoutput:{
  FeeEstLow,
  FeeEstMed,
  FeeEstHigh,
}

Note that three variables are defined for each object, which corresponds to the three fee transaction priorities (low, medium and high) defined in the previous section.

getEstimateFeeByte implements a check for filled block space. If past blocks are not filled up to a certain threshold, then the minimum fee of the transaction should be recommended:

if(wavg(sizes of 20 last blocks)) > 12.5 KB || sizeof(last block) > 14.8 KB)
      FeeEstPerByte.Low = EMAoutput.FeeEstLow
      FeeEstPerByte.Med = EMAoutput.FeeEstMed
      FeeEstPerByte.High = EMAoutput.FeeEstHigh
else
      FeeEstPerByte.Low = 0
      FeeEstPerByte.Med = 0
      FeeEstPerByte.High = 0

where wavg() stands for the weighted average. In this case, the last block has a weight equal to 1 and the weights decrease by 10% when iterating towards the genesis block.

As shown in the pseudo-code above, two thresholds affect the output of the function:

  1. The weighted average size of the last 20 blocks is larger than 12.5 KB, which is roughly the maximum size of block (15 KB) minus a full vote transaction.

  2. The size of the last block received is larger than 14.8 KB, which implies that, with high probability, there are unconfirmed transactions waiting in the transaction pool.

The object EMAoutput is the output of the mentioned EMAcalc function that computes the EMA estimation. This function calculates a new fee estimation every time a new block is received (or generated by the node) and stores it. Based on the model shown in the Mathematical description section, it takes as an input this last block and the set of estimates previously stored. The used parameters are described in the following subsection.

Parametrization of the EMA estimation function

  • offset :
    The node has to keep in its database its last set of estimates, EMAoutput. If the node crashes or is restarted, it uses this set of estimates as offset and proceeds to compute the estimates until the last block during the synchronization process.

  • α:
    We set α = 0.03406, which implies a half-life decay (d = 0.5) in the estimation after 20 blocks (B = 20).

  • feeh:
    Considering that the last block was received at height h, feeh is computed differently to calculate the three estimates contained in EMAoutput:

    • For FeeEstLow, feeh is equal to 0 if the size of the last block is less than 12.5 KB. Otherwise, it is equal to the minimum feePriority of all the transactions in this block.
    • For FeeEstMed, feeh is the average of the feePriority per byte of all the bytes[2] in the last block over the 25th percentile and below or equal the 75th percentile of the maximum block payload (15 KB).
    • For FeeEstHigh, feeh is the maximum of two values:
      • The average of the feePriority per byte of all the bytes[2] in the last block above the 80th percentile of the maximum block payload (15 KB)
      • (1.3* FeeEstMed + 1) Beddows per byte.

Note that this implies to have three parallel and independent EMA computations to get the three priority estimates.

Refer to the Appendix B for a numerical example of the calculation of EMAoutput with the mentioned parameters.

[2] Assuming that the transactions in the block are descendingly ordered by feePriority, each byte in the maximum block payload will have an associated feePriority. We assume feePriority = 0 for the cases of unused bytes or bytes used by transactions paying exactly the minimum fee.

Locally: User’s wallet

As said before, the wallet queries the fee estimation information from the connected node (the FeeEstPerByte object described before) to output the estimated transaction fee.

A function computes the estimated transaction fee taking three input arguments:

  1. The FeeEstPerByte object received from a connected node.
  2. A parameter ,priority, to specify the fee priority requested by the user.
  3. The transaction object, trs, to be transmitted.

The priority parameter can take three values, one for each of the fee priorities. Then, the function outputs the estimated transaction fee as:

if(priority = Low priority)
       output = trs.minFee + FeeEstPerByte.Low * sizeof(trs)
else if(priority  = Med priority)
       output = trs.minFee + FeeEstPerByte.Med * sizeof(trs) + minFeePerByte * IsNonZero(FeeEstPerByte.Med) * rand()
else if(priority  = High priority)
       output = trs.minFee + FeeEstPerByte.High * sizeof(trs) + minFeePerByte * IsNonZero(FeeEstPerByte.High) * rand()

output = ceil(output)

where minFeePerByte is the minimum fee per byte required by the protocol and rand() is a pseudo-random number in the range [0,1]. This function can be implemented with the Math.random() method. The third summand in the medium and high priority cases is used to introduce a tie-break when the estimation of the fee is flat (only applies when IsNonZero() = true).

Moreover, the output of the function is capped by the currently defined static fees (except for the dapp transaction type), namely:

fees: {
  send: 10000000,
  vote: 100000000,
  secondsignature: 500000000,
  delegate: 2500000000,
  multisignature: 500000000,
  dapp: 5000000000
}

Backwards Compatibility

This change is backwards compatible both for the nodes and the wallet. However, nodes should be encouraged to update to the algorithm implementation version for usability reasons.

If a client queries a node without the algorithm implemented, the node will send back an “invalid request” response. In this case, users will have to connect to a different node, or in the worst case, they will need to guess the fee to spend without any guidance.

Reference Implementation

TBD

Appendix A: Alternative way of implementing the algorithm

Another considered possibility is to implement the whole algorithm locally as a function that pulls all the required data from the blockchain every time a fee needs to be estimated.

In this case the mathematical model is:

feeEsth = α [feeh + (1- α) feeh-1 + (1- α)2 feeh-2 … + (1- α)k-1 feeh-k-1] + (1- α)koffset

where each of the feei parameters in the equation signifies the fees at the block at height i, as defined for the different priorities in the Parametrization of the EMA estimation function subsection.

In this case, the function requests the fees of the previous k blocks. Every new call to the function is a new estimation of the fee.

Advantages:

  • No need to rely on nodes to get the estimation.
  • Easier to update and improve: No need to update the Lisk node.

Drawbacks:

  • Accuracy: EMA has limited data, until k blocks in the past.
  • Accuracy: offset may have a significant impact.
  • Efficiency: It queries full blocks information from nodes every time someone wants to get an estimation. This may overload the queried node.

Appendix B: Numerical Example of the EMA Algorithm

Assume a node implementing the changes of this proposal which has been running (receiving and/or generating blocks) for a considerable amount of time. In its database, the output of the last EMA estimation function, EMAoutput_db, is stored as (in Beddows):

EMAoutput_db:{
  FeeEstLow = 0
  FeeEstMed = 1000
  FeeEstHigh = 2000
}

Assume the considered node receives a new valid block at height = h, with a total of 71 transactions ( trs1, trs2, trs3, …, trs71) and a size of approximately 13.5 KB. For each of these transactions we have its corresponding size and feePriority value:

  • Transactions from trs1 to trs60 are of type 0, 125 bytes of size, and feePriority = 0 Beddows/byte.
  • Transactions from trs61 to trs63 are of type 0, 125 bytes of size, and feePriority = 1000 Beddows/byte.
  • Transaction trs64 is of type 3, 2334 bytes of size, and feePriority = 1000 Beddows/byte.
  • Transaction trs65 is of type 0, 189 bytes of size, and feePriority = 1200 Beddows/byte.
  • Transaction trs66 is of type 1, 153 bytes of size, and feePriority = 1200 Beddows/byte.
  • Transaction trs67 is of type 1, 125 bytes of size, and feePriority = 1500 Beddows/byte.
  • Transaction trs68 is of type 3, 2270 bytes of size, and feePriority = 1800 Beddows/byte.
  • Transaction trs69 is of type 0, 125 bytes of size, and feePriority = 2000 Beddows/byte.
  • Transaction trs70 is of type 0, 253 bytes of size, and feePriority = 4000 Beddows/byte.
  • Transaction trs71 is of type 0, 189 bytes of size, and feePriority = 8000 Beddows/byte.

As the node just received a new block, the EMA estimation function is called to calculate the new set of fee estimates. In particular, each of the estimates for the new EMAoutput are calculated as:

  • For FeeEstLow, feeh = 0, which is the minimum feePriority in the block. Then:
FeeEstLow = 0 Beddows/byte
  • For FeeEstMed, we have to calculate feeh, which is the average of the feePriority per byte of all the bytes above the 11250th byte (25th percentile) and below or equal the 3750th byte (75th percentile) of the maximum block payload. Note that the transactions are descendingly ordered by feePriority. These bytes correspond to the transactions from trs20 to trs64. Hence:

feeh =
(1889 * 1000 + 3 * 125 * 1000 + 41 * 125 * 0) / 7500 = 301.9 Beddows/byte

Note that only 1889 bytes of trs64 fall below or equal the 75th percentile. Now we can compute FeeEstMed:

FeeEstMed = 0.03406 * 301.9 + (1 - 0.03406) * 1000 = 976.2 Beddows/byte
  • For FeeEstHigh, we have to calculate the average of the feePriority per byte of all the bytes above 3000th byte (80th percentile). These bytes correspond to the transactions from trs66 to trs71. The average of their feePriority is:

feeh = (189 * 8000 + 253 * 4000 + 125 * 2000 + 2270 * 1800 + 125 * 1500 + 38 * 1200) / 3000 = 2364.4 Beddows/byte

Note that only 38 bytes of trs66 fall above the 80th percentile. Now we can compute FeeEstHigh:

FeeEstHigh = 0.03406 * 2364.4 + (1 - 0.03406) * 2000 = 2012.4 Beddows/byte

Hence, the new EMAoutput output is:

EMAoutput:{
  FeeEstLow = 0
  FeeEstMed = 976.2
  FeeEstHigh = 2012.4
}

I just created the pull request for this proposal on GitHub: https://github.com/LiskHQ/lips/pull/16

Hi, I was wondering if there is a reference implementation yet for this fee estimation algorithm. As far as I can tell, it has not been integrated into lisk-core or lisk-sdk. Thanks!

Hi! yes, this was implemented for Lisk Service version 0.2.0.

The PR with the implementation, that I reviewed myself, is this one:

1 Like

That’s great and thanks for the prompt response! I was wondering if there are plans to add this functionality into lisk-core. lisk-core basically fulfills all the needs that I have, except for the fee estimation, of which I’d rather not have to make a manual copy implementation.

As far as I understand, the fee estimation functionality (getEstimateFeeByte function at lisk-service/core.js) is implemented to work as a plugin to lisk-core. This function needs to get the information of the previous blocks so it assumes some running application will provide it.

This is just my basic understanding of this implementation. For a proper answer and guidance on the topic, please post your doubts in the Lisk dev forum: https://dev.lisk.com/

You can also it in the Lisk.chat in the developers channel on Discord: Discord

1 Like