Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions elip-0000.mediawiki
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<pre>
ELIP: ???
Layer: Applications
Title: Issued Asset Fees
Author: Tom Trevethan <ttrevethan@blockstream.com>
Comments-Summary: No comments yet.
Comments-URI: https://github.com/ElementsProject/elips/wiki/Comments:ELIP-0???
Status: Draft
Type: Standards Track
Created: 2025-09-02
License: BSD-3-Clause
</pre>

==Introduction==

===Abstract===

This document proposes a modification to Elements to enable transaction fees to be paid in issued assets in addition to the <code>policyAsset</code> (or pegged asset). To enable this, the specific issued assets that will be accepted for fees and the corresponding fee rate (relative to the enforced <code>policyAsset</code> rate) must be agreed and configured by all block creators and also communicated to users and implemented in wallets. To enable the use of issued asset fees requires changes to policy rules for transaction relay mempool acceptance and ordering, and changes to block creation to enable issued asset fees to be paid to specified destinations in coinbase outputs. Issued asset fee acceptance and payout in the coinbase transaction is valid within the current consensus rules and so none of the features described here require any consensus fork to deploy. In addition to these changes to policy and block creation, this document also proposes features to enable both publication of accepted assets and fee rates on-chain for use by wallets, and a method to update the fee asset policy applied by nodes remotely via an authenticated on-chain transaction.

===Copyright===

This document is licensed under the 3-clause BSD license.

===Motivation===

In Elements, transaction fees are required in order to prevent denial of service attacks and to prioritize transactions for limited block space and are specified by an explicit unblinded output (signified by an empty <code>scriptPubKey</code>). In the current Elements implementation, policy rules for transaction relay and mempool acceptance require that the <code>assetID</code> of this fee output be equal to the <code>policyAsset</code>. The current consensus rules however allow for any <code>assetID</code> for the fee asset (so long as the transaction is otherwise valid). The current consensus rules also allow for coinbase outputs for any asset with values equal to or less than the total fee outputs for that asset for all transactions in a block. However the current Elements code will only create a block with a single spendable coinbase output for the total of <code>policyAsset</code> fees.

These current restrictions limit the usefulness of the Elements platform for transacting in issued assets, as <code>policyAsset</code> is always then required for the payment of transaction fees. For example, for a Bitcoin sidechain (e.q. Liquid) there are many issued assets for tokenized currencies (e.g. USDT) which many users will use for making payments. However they must obtain an amount of the <code>policyAsset</code> (e.g. LBTC) before they can transact, and the fees will be priced in BTC instead of USD. This leads to a poor user experience when transacting in tokenized stablecoins, as a new user will be required to purchase LBTC before they are able to spend USDT that they have received in their wallet. In addition, the value of transaction fee required will not be explicit in terms of USDT and depend on the current USD value of BTC. By enabling fees to be paid in specified issued assets and at specified rates, user wallets can transact in an issued asset like USDT seamlessly, with predictable and transparent fees.

In addition to enabling the issued asset transaction fees to be accepted, and payment of these fees in the coinbase of new blocks, there needs to be a method for wallets and users to know which assets are accepted by block creators and at what rates. The simplest and most robust way of achieving this is with a publication of the accepted assets and rates by block creators on chain, in the coinbase transaction - this removes the requirement for separate servers to publish and relay this data.

Finally, a method is required to configure which assets and corresponding fee rates will be applied to policy on relaying and block creation nodes. These can be set with direct access to the node, either via the node configuration or the RPC interface. However, the accepted assets and fee rates may need to be updated frequently, and it may be impractical and have security implications to have direct access to functionary and bridge nodes to perform these updates via the RPC interface. Therefore, a method to perform this update remotely via an on-chain transaction is also required. Individual nodes can then be configured with a 'controller' script that authenticates changes to the asset fee policy without requiring direct access to any individual nodes.

==Design==

===Overview===

Each node has complete control over which assets and at what fee rates to accept as policy. However, in practice all block creators and relaying nodes should agree to apply the same asset fee policy in order to provide a reliable service for users. The assets and fee rates accepted by any node can be set as part of the node configuration, specifying the <code>assetID</code>, the relative rate multiplier (and optionally the coinbase destination for the fee collection by the block creator) before initialization, or this can be set dynamically at runtime via a new RPC. In addition, a new RPC will be added to query the current assets, rate multiplier (and destinations) currently being applied to policy by any node.

For fee outputs in an issued asset, the fee rate (in units/vbyte) will be scaled to determine an 'effective rate'. This effective rate is then used in place of the <code>policyAsset</code> fee rate for the purpose of meeting relay and mempool minimums and for mempool ordering.

E.g. An issued asset is USDT. The current accepted exchange rate between USDT and LBTC (the policyAsset) is $114,171.23 to 1 LBTC. The issued asset fee rate multiplier for this ratio is then set to 1/114,171.23 = 0.00000876 (rounded to a precision of 1e-8). For a transaction that uses the USDT asset for the fee, the fee rate (in units/vbyte) multiplied by this number must be greater than or equal to the minimum fee rate (in sat/vbyte) in LBTC applied to policy.
If the current minimum relay fee rate is 1 sat/vbyte in LBTC, then the minimum fee rate required in USDT would be 1/0.00000876 = 114156 units/vbyte (rounded up to the nearest integer). To relay this transaction, a client will determine the fee rate in the USDT asset, multiply it by 0.00000876 to calculate the effective rate and then verify it is greater than or equal to 1 sat/vbyte.

===Specification===

====Issued asset fees====

Accepted assets, rate multipliers (and optional destination) are stored in a vector <code>assetFeeRates</code> of <code>CAssetFeeRate</code> objects.
This object contains the <code>CAsset</code> asset ID, the <code>int64_t</code> fee rate multiplier and optional <code>CScript</code> fee destination.

For each supported issued asset, there is a specified fee rate multiplier: <code>issued_asset_fee_rate_multiplier</code> (a float). This is represented internally as a fixed precision <code>int64_t</code> number <code>issued_asset_fee_rate_multiplier_int64</code> (the number of decimal places supported is specified by the <code>int64_t</code> parameter <code>ASSET_MULTIPLIER_SCALE_FACTOR</code> (default <code>100000000</code>) which is applied to all asset multipliers.

<code>
issued_asset_fee_rate_multiplier_int64 = static_cast<int64_t>(std::ceil(issued_asset_fee_rate_multiplier * ASSET_RATE_SCALE_FACTOR))
</code>

This value scales the calculated the fee rate of a transaction that has an issued asset fee to give an <code>effective_fee_rate</code>.

<code>
effective_fee_rate = (asset_fee_rate * issued_asset_fee_rate_multiplier_int64) / ASSET_MULTIPLIER_SCALE_FACTOR
</code>

Where <code>asset_fee_rate</code> is the fee rate calculated using the transaction vsize and the amount of the issued asset fee output. For every transaction in the mempool that has an issued asset fee, the <code>effective_fee_rate</code> is used in place of the policyAsset fee rate for the purposes of mempool ordering and minimum mempool fee rate calculation.

The value of <code>issued_asset_fee_rate_multiplier_int64</code> must be between 1 and <code>MAX_ASSET_MULTIPLIER</code>

By default <code>MAX_ASSET_MULTIPLIER = 10^15</code>. Scaled as a fixed precision number, this enables values of <code>issued_asset_fee_rate_multiplier</code> between 0.00000001 and 10,000,000.

The value of <code>(asset_fee_rate * issued_asset_fee_rate_multiplier_int64)</code> cannot exceed the maximum allowed value of <code>int64_t</code>. If it does, then the <code>effective_fee_rate</code> is set to maximum allowable value of <code>std::numeric_limits<uint64_t>::max() / ASSET_RATE_SCALE_FACTOR = 92233720368</code> (which is 922.3372 LBTC/vbyte).

To calculate the value of an issued asset fee output required for a transaction of size <code>vsize</code> where the current minimum <code>policyAsset</code> fee rate is <code>policy_asset_rate</code>:

<code>
fee_amount = vsize * policy_asset_rate * (1 + ((ASSET_RATE_SCALE_FACTOR - 1) / issued_asset_fee_rate_multiplier_int64))
</code>

The fee asset and rate multiplier can be configured for an individual node with the option:

<code>-setfeeassetrate=<assetID:multiplier-script></code>: set the <code>assetID:multiplier-script</code> applied to relay and mempool policy for this node.

The multiplier is supplied as a floating point number (<code>issued_asset_fee_rate_multiplier</code>) which is converted to fixed point integer by ceiling multiplication with ASSET_MULTIPLIER_SCALE_FACTOR.

This can be repeated for additional fee assets and rates. The maximum number of issued asset fee multipliers allowed is set by <code>MAX_ISSUED_ASSET_FEE_SIZE</code> (default 10).

A new accepted fee asset can be added on demand at runtime with the RPC, or an existing asset updated with:

<code>updatefeeassetrate</code> add new assetID, multiplier, (optional) destination script to apply to node policy or update the rates of existing configured assets dynamically.

<code>getassetfeerate</code> get all assetID, multiplier, script applied by this client.

Note: Consensus enforces that any individual fee output, and the total sum of fee values in a block do not exceed <code>MAX_MONEY</code>. Issued asset fees could in theory exceed this, as individual assets might have much larger issuance amounts than <code>MAX_MONEY</code>. Therefore, currently in <code>BlockAssembler</code> the inclusion of issued asset fee paying transactions in a block will be limited so that the total value of fee outputs will not exceed <code>MAX_MONEY</code> when generating a new block.

====Fee rate publication====

Asset fee rates multipliers (and optional destination) encoded in additional coinbase <code>OP_RETURN</code> outputs as follows:

* 4 bytes: <code>AFEE</code>
* 32 bytes: asset ID
* 16 bytes: fee rate multiplier (uint_64 encoded)
* (up to) 35 bytes: fee destination script

The maximum number of additional outputs is limited to <code>MAX_ISSUED_ASSET_FEE_SIZE</code>. The maximum size for transactions included in a generated block must be reduced by the additional space taken by the additional coinbase outputs.

Config option:

<code>-writefeeassetrate=n</code> Write issued asset fee rate multipliers accepted as policy to the coinbase of created blocks with interval <code>n</code> (default: 0).

If <code>n</code> is 0 then fee assets and rate multipliers are not written.

If <code>n</code> > 0 then any accepted assets and fee rates set via RPC or config are published every <code>n</code> blocks.
Any fee rates updated by <code>setassetfeerate</code> or controller script are not applied to policy until the end of an epoch of <code>n</code> blocks.

A new RPC <code>getsignerfeerates</code> retrieves the latest published accepted assets and rate multipliers.
<code>getassetfeerate</code> has a single boolean argument to either retrieve the latest updated list of assets and fee rate multipliers, or the list applied in the current epoch.

On node restart, the cached rates are updated from the latest publication.

====Controller fee asset rate updates====

Feature for issued asset fees that enables an external controller to set issued asset fee policy via an onchain transaction, with asset fee rate multipliers encoded in <code>OP_RETURN</code> outputs.
Any node can be configured to apply issued asset fee policy that is set by a controller (this does not need to be a single entity, but can be defined by any multisig script). This is set with a config option:

<code>-confeeassetrate=scriptPubKey</code>

where <code>scriptPubKey</code> is the script that is used by the controller(s) to set the asset and rate multipliers in a transaction.
<code>-conassetfeerate</code> cannot be set at the same time as <code>-setassetfeerate</code> (this will generate an error).
A valid controller transaction must spend from an output where the <code>scriptPubKey</code> is also the same controller script (this is used as an authentication mechanism).

The format for the <code>OP_RETURN</code> encoding is that same as that used for the fee rate publication.

==Backwards Compatibility==

No issues for backwards compatibility - all existing transactions with policy asset fees are unaffected by these features. No changes to consensus rules are required.

==Test Vectors==

* Configure issued asset with fee rate of <code>issued_asset_fee_rate_multiplier = 0.5</code>

<pre>-setfeeassetrate=e86ed2437731d8ebb6bf457ee95a7f50e0bdfb41d319c69af046c52c7f25aba0:0.5</pre>

* Get configured fee asset rates

<pre>getassetfeerate</pre>

Result:

<pre>[{'asset': 'e86ed2437731d8ebb6bf457ee95a7f50e0bdfb41d319c69af046c52c7f25aba0', 'rate': Decimal('0.5'), 'script': ''}]</pre>

* Transaction with 1 input and 2 outputs (+ 1 fee output) with asset fee multiplier of 0.5:

** vsize: 257

** Fee: 514

<pre>0200000001016f90831e415a738df732b62dd0b76e87bbe489096e43c892bf3dcbbfd173778d0000000000feffffff0301a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee8010000000002faf08000160014c5439a67eaa99a308719b544f327a942ae02e6a701a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee8010000000002f967de0016001467c8c91e8357e1e24d1721c4535a99608e47beb701a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee801000000000000020200006c0000000000024730440220031c1aa256e40a89cf622374e1ef281870a6abb6f588411800fe885f5195a49e02204f3c37cfe2318db87ac7ce4d7da70682e3477653f6222383ab72ea259a3b1fae0121029896a2fe9c710b8b71488b90038d3974144dc21c9c8750459162dc37d141459300000000000000</pre>

* Update issued asset fee rate multiplier to 0.1 with RPC

<pre>updatefeeassetrate e86ed2437731d8ebb6bf457ee95a7f50e0bdfb41d319c69af046c52c7f25aba0 0.1</pre>

* Get configured fee asset rates

<pre>getassetfeerates</pre>

Result:

<pre>[{'asset': 'e86ed2437731d8ebb6bf457ee95a7f50e0bdfb41d319c69af046c52c7f25aba0', 'rate': Decimal('0.1'), 'script': ''}]</pre>

* Transaction with 1 input and 2 outputs (+ 1 fee output) with asset fee at rate multiplier 0.1:

** vsize: 257

** Fee: 2570

<pre>0200000001015ce5df8217dfa572d4539d0ae05178be26c27dbc43af35f6877f0160c8d329360100000000feffffff0301a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee8010000000001312d0000160014fc060cafa93c7f4d5e0963ba08c55540c28361bf01a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee8010000000001c830d4001600141f79c72bcdad5c20c17337c1f22c3b41f156c53d01a0ab257f2cc546f09ac619d341fbbde0507f5ae97e45bfb6ebd8317743d26ee8010000000000000a0a00004100000000000247304402205bc1f97638e5b1932ba6df702e938bd1cfdc6da46a4db8511fe172f821f102c502206a6fbe66d81e89bd78b4157d58af15e8c940e0c0d797cbe91535fb31683e013601210291d9c10cf976e932db8ffe0f2bf1d9c1574ea3affe2e0f98ff15e1b328eb860300000000000000</pre>

* Configuration for fee asset rate publication:

<pre>-writefeeassetrate=100</pre>

<pre>updatefeeassetrate b92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a22 1.86</pre>
<pre>updatefeeassetrate af6e22da7b20de2da56c6726c4a036c73f777c8fb5bfc356892c01a9a3dd58d3 3.781</pre>

Coinbase transaction:

<pre>0200000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0401640101ffffffff0401230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000000002c6a2aafeeaf6e22da7b20de2da56c6726c4a036c73f777c8fb5bfc356892c01a9a3dd58d3000000008d1ba84001230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000000002c6a2aafeeb92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a22000000001689592001230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000012a05f200001976a9149d8419680d03a0ac4133beb9e097d5c9ff7babbc88ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21a9ed5aa9aac025ce73291c124e2ccfd44ae60837d1f20a8573318427075444287de800000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000</pre>

Output 1:

<pre>afeeaf6e22da7b20de2da56c6726c4a036c73f777c8fb5bfc356892c01a9a3dd58d3000000008d1ba840</pre>

Output 2:

<pre>afeeb92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a220000000016895920</pre>

* Controller transaction

<pre>
controler_script = "00145956bc071791295bfd5161ddb770aa536289b3e3"
controller_addr = "ert1qt9ttcpchjy54hl23v8wmwu922d3gnvlrz7sqx7"
private_key = "cP3N3Z3rMTo4L2gNRDWmzo8nGNyVNnWwFX9DRVPqwcPnMWE8Rn33"
</pre>

Configuration

<pre>-confeeassetrate=00145956bc071791295bfd5161ddb770aa536289b3e3</pre>

Fee Asset setting:

<pre>asset_id = b92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a22</pre>

rate = 1.0

Encoded fee asset hex string:

<pre>afeeb92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a220000000005f5e100</pre>

Controller transaction:

<pre>020000000001500d712c870c96c0bec05f41d765f4f5f203bd8d5b7ea6ef333b53b8c2c2d164000000006a47304402207f1b2c580ee052b8d0da06a139cd1e910ec93c8b11585d692646e6267b8354c4022045caab622e3adbae204263dfdc86e341fd9f3c013cbf2234fdea2c8ee2ba0d81012103ba4a2b1f401eb59e1e6b104f8043ce41b38b65bd24c10edb3df8863b0241e5afffffffff0301230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000012a046b60001600145956bc071791295bfd5161ddb770aa536289b3e301230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000000002c6a2aafeeb92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a220000000005f5e10001230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000000000186a0000000000000</pre>

<pre>getassetfeerate</pre>

<pre>[{'asset': 'b92b2e64772d8edc7703534fc5028906ab1468a7497af9e8620999132af96a22', 'rate': 1, 'script': ''}]</pre>

<references />