Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Latest commit

Β 

History

History
411 lines (313 loc) Β· 22.3 KB

lip-0048.md

File metadata and controls

411 lines (313 loc) Β· 22.3 KB
LIP: 0048
Title: Introduce Fee module
Author: Maxime Gagnebin <maxime.gagnebin@lightcurve.io>
        Mitsuaki Uchimoto <mitsuaki.uchimoto@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-fee-module/318
Status: Active
Type: Standards Track
Created: 2021-08-09
Updated: 2024-01-04
Requires: 0051

Abstract

The Fee module is responsible for handling the fee of transactions. It allows chains to choose the token used to pay the fee and to define a minimum fee for transactions to be valid.

Copyright

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

Motivation

This LIP defines the fee system in a modular way, as currently used in the Lisk ecosystem. The fee handling is implemented in a separate module to allow sidechains to freely update or replace the fee handling module, possibly to implement a more complex fee structure, without needing to modify or update the Token module.

Rationale

Transaction Fees

Blockchain transaction fees are instrumental for various reasons, including:

  • Paying for state growth, node computation, and blockspace utilization.
  • Preventing spamming a blockchain with unlimited zero-cost transactions.
  • Incentivizing validators who operate the network and maintain its security, by providing them with a part of collected fees.

Consequently, each transaction in the Lisk ecosystem has a transaction fee associated with it, as defined by the fee property of the transaction schema in LIP 0068. Each chain can configure the token used to pay fees, as defined by TOKEN_ID_FEE. On the Lisk mainchain, the token used for transaction fees is the LSK token.

The fee system proposed here automates the calculation of blockspace fees using a minimum fee approach: The transaction fee should be greater than or equal to a threshold that depends on the size of the transaction. Fees associated to other factors, such as state growth or computational effort, are determined by the corresponding parts of the protocol, e.g., fee for registering a validator in the PoS module. The fee module manages these additional fees through the payFee method.

Minimum Fee per Transaction

As introduced in LIP 0013, all transactions must have a transaction fee that is at least equal to a minimum fee. This fee corresponds to the cost of using blockspace and is therefore dependent on the size of the transaction, with the proportionality constant of MIN_FEE_PER_BYTE:

trsSize = length(encodeTransaction(trs))
minFee = MIN_FEE_PER_BYTE * trsSize

The minimum fee is always subtracted from the part of the fee that goes to the block generator, as we explain below. This way, block generators do not have the option to include zero-cost transactions in the blocks they generate, unless minimum fee is set to zero.

Note that chains can freely configure MIN_FEE_PER_BYTE, which can even be set to zero. The MIN_FEE_PER_BYTE value cannot be changed, unless a hard fork occurs. For this reason, we allow sidechains to define a period of MAX_BLOCK_HEIGHT_ZERO_FEE_PER_BYTE blocks after the genesis block, for which the minimum fee does not apply.

This could, for example, be beneficial for sidechains that want to use LSK as the fee token. Initially, there is no LSK on the sidechain, so the sidechain cannot process transactions unless MIN_FEE_PER_BYTE is set to zero, which is generally not desirable since it essentially disables the feature of minimum fee for the chain. By granting an initial exemption period of MAX_BLOCK_HEIGHT_ZERO_FEE_PER_BYTE blocks, users have a window of time to transfer LSK tokens to the sidechain, after which the initially set value of MIN_FEE_PER_BYTE would start being applied.

Additional Fees

In principle, any transaction that results in a state increase or significant computational effort could require a fee. Since the current implementation does not provide an automatic way to charge fees in these cases, we introduce the payFee method.

This method can be invoked by other modules to charge an additional fee when a state increase is triggered during the execution of a transaction, e.g., when a new validator is registered in the PoS module, a new sidechain is registered, or a user account is initialized in the Token module:

Fee.payFee(USER_ACCOUNT_INITIALIZATION_FEE)

In addition, developers may decide to call the payFee method for transactions that require extensive computation, e.g., when verifying an inclusion proof, or if any other resource is extensively used during transaction execution.

Transaction Fee Processing

We refer to the sum of the minimum fee and the additional fees processed by the payFee method as the "consumed" part of the fee. The remainder of the fee, called the "available" part of the fee, is transferred to the validator as a reward. In this way, validators are assumed to prioritize transactions with a higher available fee in case of network congestion. After the transaction is executed, consumed fees are either burned or transferred to a dedicated pool address.

The option to transfer the consumed fees to a dedicated pool address ADDRESS_FEE_POOL could be beneficial for sidechains, as fees collected this way can be used as an incentive for validators and other protocol participants for various purposes: Funding a community pool for on-chain governance spending, sharing funds proportionally among validators per round, incentivizing ecosystem developers, supporting chain maintenance and improvement, rewarding users' staking and voting activities, etc.

In the Lisk mainchain, the consumed fees are always burned. This reduces the total supply, creating a deflationary pressure that works against inflation.

Cross-chain Message Fees

Cross-chain messages (CCMs) facilitate interoperability by enabling cross-chain transfer of data. Each CCM contains a message fee, intended to cover the cost of transaction processing on the receiving chain (see LIP 0049).

Since managing message fees requires checking and updating the escrow balances for the message fee token, this task is assigned to the Token module whose payMessageFee method charges message fees in the sending chain. In the receiving chain, the Token module assigns the message fee to the relayer account and updates escrows if necessary, and then the Fee module handles the message fee processing in a similar way as it handles transaction fees.

Minimum Fee per CCM

For protocol reasons (see, e.g., bounce and terminateChain methods in LIP 0045), the Interoperability module allows the message fee to be zero. Consequently, the choice of setting a minimum message fee is delegated to each module that provides its own cross-chain commands. Nevertheless, there is a minimum fee in case the user wants the failed CCM to be returned (see bounce method in LIP 0045). Thus, even though this fee is not mandatory, it is advisable to treat it as a suggested minimum fee. Note also that the CCM sender should include any additional fees necessary for the CCM execution, e.g., to pay for the user account initialization on the receiving chain.

Message Fee Processing

Processing of message fees is similar to that of transaction fees. The only essential difference is that the consumed part of the message fee contains only the so-called additional fees, as there is no mandatory minimum fee. The consumed part is handled in the same way as for transactions, i.e., it is either burned or transferred to a fee pool, depending on the chain configuration.

The available part of the message fee is left to the relayer to cover the cost of CCU execution (recall that a CCU is a regular transaction, therefore the relayer has to pay transaction fees for posting the CCU in the receiving chain), as well as to incentivize its participation in CCM relaying.

Finally, note that, in contrast to including transactions in a block, a relayer cannot prioritize CCMs with higher fees as the interoperability protocol requires them to be sent in order. This implies that users have no reason to pay higher message fees than are required for successful CCM execution. As a consequence, users understand that CCMs will be delivered as soon as a relayer decides to post the CCU in the target chain, eliminating any concerns about potential delays due to priority-related reasons.

Specification

Notation and Constants

We define the following constants:

Name Type Value Description
General Constants
MODULE_NAME_FEE string "fee" Module name of the Fee module.
EVENT_NAME_GENERATOR_FEE_PROCESSED string "generatorFeeProcessed" Event name of the GeneratorFeeProcessed event.
EVENT_NAME_RELAYER_FEE_PROCESSED string "relayerFeeProcessed" Event name of the RelayerFeeProcessed event.
EVENT_NAME_INSUFFICIENT_FEE string "insufficientFee" Event name of the InsufficientFee event.
LENGTH_ADDRESS uint32 20 The length of an address in bytes.
LENGTH_TOKEN_ID uint32 8 The length of a token ID in bytes.
Configurable Constants Mainchain Value
MIN_FEE_PER_BYTE uint64 1000 Minimum amount of fee per byte required for transaction validity.
MAX_BLOCK_HEIGHT_ZERO_FEE_PER_BYTE uint32 0 Block height ending the MIN_FEE_PER_BYTE = 0 initial period.
TOKEN_ID_FEE bytes OWN_CHAIN_ID[0:1] + '00000000000000' Token ID of the token used to pay the transaction fees.
ADDRESS_FEE_POOL bytes None Address of the fee pool.

Type Definitions

We use the definition of the following types:

Name Type Validation Description
TokenID bytes Must be of length LENGTH_TOKEN_ID. Used for token identifiers.

Furthermore, for the rest of this LIP we indicate with ctx the execution context which is passed as extra input to each method call.

Functions from Other Modules

Calling a function fct from another module (named module) is represented by module.fct(required inputs).

Fee Module Store

The Fee module does not store information in the state.

Commands

The Fee module does not contain any commands.

Events

GeneratorFeeProcessed

This event has name name = EVENT_NAME_GENERATOR_FEE_PROCESSED. The event is emitted when a transaction fee is assigned to the generator. Event's data includes the amount of burnt fee tokens and the amount of fee tokens paid to the block generator.

Topics
  • senderAddress: the address of the account paying the fee.
  • generatorAddress: the address of the generator of the block receiving the fee.
Data
generatorFeeProcessedEventDataSchema = {
    "type": "object",
    "required": ["senderAddress", "generatorAddress", "requiredAmount", "generatorAmount"],
    "properties": {
        "senderAddress": {
            "dataType": "bytes",
            "length": LENGTH_ADDRESS,
            "fieldNumber": 1
        },
        "generatorAddress": {
            "dataType": "bytes",
            "length": LENGTH_ADDRESS,
            "fieldNumber": 2
        },
        "requiredAmount": {
            "dataType": "uint64",
            "fieldNumber": 3
        },
        "generatorAmount": {
            "dataType": "uint64",
            "fieldNumber": 4
        }
    }
}

RelayerFeeProcessed

This event has name name = EVENT_NAME_RELAYER_FEE_PROCESSED. The event is emitted when a message fee is assigned to the CCU relayer. Event's data includes the amount of burnt fee tokens and the amount of fee tokens paid to the relayer.

Topics
  • relayerAddress: the address of the relayer.
Data
relayerFeeProcessedEventDataSchema = {
    "type": "object",
    "required": ["ccmID", "relayerAddress", "requiredAmount", "relayerAmount"],
    "properties": {
        "ccmID": {
            "dataType": "bytes",
            "length": LENGTH_HASH,
            "fieldNumber": 1
        },
        "relayerAddress": {
            "dataType": "bytes",
            "length": LENGTH_ADDRESS,
            "fieldNumber": 2
        },
        "requiredAmount": {
            "dataType": "uint64",
            "fieldNumber": 3
        },
        "relayerAmount": {
            "dataType": "uint64",
            "fieldNumber": 4
        }
    }
}

InsufficientFee

This event has name name = EVENT_NAME_INSUFFICIENT_FEE. The event is emitted when there is not enough transaction or cross-chain message fee left.

Topics

This event has no extra topics (only the default transaction or cross-chain message ID).

Data
insufficientFeeDataSchema = {
    "type": "object",
    "required": [],
    "properties": {}
}

Protocol Logic for Other Modules

payFee

def payFee(amount: uint64) -> None:
    # ctx.ccmProcessing is set by the Interoperability module during the CCM processing.
    # In particular, before the CCM is processed, ctx.ccmProcessing is set to True.
    # After the CCM is processed, ctx.ccmProcessing is set to False.
    if ctx.ccmProcessing:
        ctx.availableCCMFee -= amount
        if ctx.availableCCMFee < 0:
            ctx.availableCCMFee = 0
            emitPersistentEvent(
                module = MODULE_NAME_FEE,
                name = EVENT_NAME_INSUFFICIENT_FEE,
                data = {},
                topics = []
            )
            raise Exception("Cross-chain message ran out of fee.")
    else:
        ctx.availableTransactionFee -= amount
        if ctx.availableTransactionFee < 0:
            ctx.availableTransactionFee = 0
            emitPersistentEvent(
                module = MODULE_NAME_FEE,
                name = EVENT_NAME_INSUFFICIENT_FEE,
                data = {},
                topics = []
            )
            raise Exception("Transaction ran out of fee.")

getFeeTokenID

def getFeeTokenID() -> TokenID:
    return TOKEN_ID_FEE

Block Processing

Verify Transaction

This step is run for every transaction before it is submitted to the chain. See LIP 0055 for details of block processing.

def verify(trs: Transaction) -> None:
    b = block including trs
    h = b.header.height
    if h < MAX_BLOCK_HEIGHT_ZERO_FEE_PER_BYTE:
        # Set minFee = 0 for the first MAX_BLOCK_HEIGHT_ZERO_FEE_PER_BYTE blocks.
        minFee = 0
    else:    
        trsSize = length(encodeTransaction(trs))
        minFee = MIN_FEE_PER_BYTE * trsSize
    if trs.fee < minFee:
        raise Exception("Insufficient transaction fee")
    senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
    if not Token.userSubstoreExists(senderAddress, TOKEN_ID_FEE):
        raise Exception("Account not initialized")
    if trs.fee > Token.getAvailableBalance(senderAddress, TOKEN_ID_FEE):
        raise Exception("Insufficient balance")

Before Command Execution

def beforeCommandExecute(trs: Transaction) -> None:
    senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
    Token.lock(senderAddress, MODULE_NAME_FEE, TOKEN_ID_FEE, trs.fee)
    minFee = MIN_FEE_PER_BYTE * len(encodeTransaction(trs))
    ctx.availableTransactionFee = trs.fee - minFee

After Command Execution

def afterCommandExecute(trs: Transaction) -> None:
    let b be the block including trs
    generatorAddress = b.header.generatorAddress
    senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
    # The fee paid to the generator is the difference between the fee paid by the sender and the paid fees.
    Token.unlock(senderAddress, MODULE_NAME_FEE, TOKEN_ID_FEE, trs.fee)

    if Token.userSubstoreExists(generatorAddress, TOKEN_ID_FEE):
        Token.transfer(senderAddress, generatorAddress, TOKEN_ID_FEE, ctx.availableTransactionFee)
    else:
        ctx.availableTransactionFee = 0

    burnConsumedFee = False if (ADDRESS_FEE_POOL is not None and Token.userSubstoreExists(ADDRESS_FEE_POOL, TOKEN_ID_FEE)) else True

    if burnConsumedFee:
       Token.burn(senderAddress, TOKEN_ID_FEE, trs.fee - ctx.availableTransactionFee)
    else:
       Token.transfer(senderAddress, ADDRESS_FEE_POOL, TOKEN_ID_FEE, trs.fee - ctx.availableTransactionFee)

    emitEvent(
        module = MODULE_NAME_FEE,
        name = EVENT_NAME_GENERATOR_FEE_PROCESSED,
        data = {
            "senderAddress": senderAddress,
            "generatorAddress": generatorAddress,
            "requiredAmount": trs.fee - ctx.availableTransactionFee,
            "generatorAmount": ctx.availableTransactionFee
        },
        topics = [senderAddress, generatorAddress]
    )
    ctx.availableTransactionFee = 0

Cross-chain Update Processing

Before Cross-chain Command Execution

def beforeCrossChainCommandExecution(trs: Transaction, ccm: CCM) -> None:
    # The Token module handles checks on the ccm.fee validity
    # with respect to the escrow account of the ccm sending chain.
    messageFeeTokenID = Interoperability.getMessageFeeTokenIDFromCCM(ccm)
    relayerAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]

    # The Token module beforeCrossChainCommandExecution needs to be called first
    # to ensure that the relayer has enough funds.
    Token.lock(relayerAddress, MODULE_NAME_FEE, messageFeeTokenID, ccm.fee)
    ctx.availableCCMFee = ccm.fee

After Cross-chain Command Execution

def afterCrossChainCommandExecution(trs: Transaction, ccm: CCM) -> None:
    # The fee paid to the relayer is the difference between the message fee and the paid fees.
    relayerAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
    messageFeeTokenID = Interoperability.getMessageFeeTokenIDFromCCM(ccm)
    Token.unlock(relayerAddress, MODULE_NAME_FEE, messageFeeTokenID, ccm.fee)

    burnConsumedFee = False if (ADDRESS_FEE_POOL is not None and Token.userSubstoreExists(ADDRESS_FEE_POOL, messageFeeTokenID)) else True

    if burnConsumedFee:
        Token.burn(relayerAddress, messageFeeTokenID, ccm.fee - ctx.availableCCMFee)
    else:
        # Transfer the cross-chain message fees to the pool account address, if it exists and it is initialized for the cross-chain message fee token.
        Token.transfer(relayerAddress, ADDRESS_FEE_POOL, messageFeeTokenID, ccm.fee - ctx.availableCCMFee)
    
    # No need to transfer as the remaining fee is already in the relayer account.    
    
    ccmID = sha256(encodeCCM(ccm))
    emitEvent(
        module = MODULE_NAME_FEE,
        name = EVENT_NAME_RELAYER_FEE_PROCESSED,
        data = {
            "ccmID": ccmID,
            "relayerAddress": relayerAddress,
            "requiredAmount": ccm.fee - ctx.availableCCMFee,
            "relayerAmount": ctx.availableCCMFee
        },
        topics = [relayerAddress]
    )
    ctx.availableCCMFee = 0

Endpoints for Off-Chain Services

This module does not have any non-trivial or recommended endpoints for off-chain services.

Backwards Compatibility

This LIP defines a new Fee module, which follows the same protocol as currently implemented. Changing the implementation to include the Fee module will be backwards compatible.

Reference Implementation

Introduce Fee module