Add base and priority fee to gas oracle response

mf-gas-price-oracle-base-priority
Maxim Filonov 10 months ago
parent cf995975d3
commit e1871b0ff3
  1. 2
      CHANGELOG.md
  2. 52
      apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
  3. 79
      apps/explorer/lib/explorer/chain/block.ex
  4. 42
      apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex
  5. 40
      apps/explorer/test/explorer/chain/block_test.exs
  6. 81
      apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs
  7. 3
      config/runtime.exs

@ -4,6 +4,8 @@
### Features
- [#9202](https://github.com/blockscout/blockscout/pull/9202) - Add base and priority fee to gas oracle response
### Fixes
- [#9317](https://github.com/blockscout/blockscout/pull/9317) - Include null gas price txs in fee calculations

@ -53,8 +53,8 @@ defmodule BlockScoutWeb.API.V2.BlockView do
"uncles_hashes" => prepare_uncles(block.uncle_relations),
# "state_root" => "TODO",
"rewards" => prepare_rewards(block.rewards, block, single_block?),
"gas_target_percentage" => gas_target(block),
"gas_used_percentage" => gas_used_percentage(block),
"gas_target_percentage" => Block.gas_target(block),
"gas_used_percentage" => Block.gas_used_percentage(block),
"burnt_fees_percentage" => burnt_fees_percentage(burnt_fees, transaction_fees),
"type" => block |> BlockView.block_type() |> String.downcase(),
"tx_fees" => transaction_fees,
@ -84,24 +84,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do
%{"hash" => uncle_relation.uncle_hash}
end
def gas_target(block) do
if Decimal.compare(block.gas_limit, 0) == :gt do
elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier)
ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier))
ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float()
else
Decimal.new(0)
end
end
def gas_used_percentage(block) do
if Decimal.compare(block.gas_limit, 0) == :gt do
block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float()
else
Decimal.new(0)
end
end
def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil
def burnt_fees_percentage(burnt_fees, transaction_fees)
@ -117,18 +99,24 @@ defmodule BlockScoutWeb.API.V2.BlockView do
def count_withdrawals(%Block{withdrawals: withdrawals}) when is_list(withdrawals), do: Enum.count(withdrawals)
def count_withdrawals(_), do: nil
defp chain_type_fields(result, block, single_block?) do
case single_block? && Application.get_env(:explorer, :chain_type) do
"rsk" ->
result
|> Map.put("minimum_gas_price", block.minimum_gas_price)
|> Map.put("bitcoin_merged_mining_header", block.bitcoin_merged_mining_header)
|> Map.put("bitcoin_merged_mining_coinbase_transaction", block.bitcoin_merged_mining_coinbase_transaction)
|> Map.put("bitcoin_merged_mining_merkle_proof", block.bitcoin_merged_mining_merkle_proof)
|> Map.put("hash_for_merged_mining", block.hash_for_merged_mining)
_ ->
case Application.compile_env(:explorer, :chain_type) do
"rsk" ->
defp chain_type_fields(result, block, single_block?) do
if single_block? do
result
|> Map.put("minimum_gas_price", block.minimum_gas_price)
|> Map.put("bitcoin_merged_mining_header", block.bitcoin_merged_mining_header)
|> Map.put("bitcoin_merged_mining_coinbase_transaction", block.bitcoin_merged_mining_coinbase_transaction)
|> Map.put("bitcoin_merged_mining_merkle_proof", block.bitcoin_merged_mining_merkle_proof)
|> Map.put("hash_for_merged_mining", block.hash_for_merged_mining)
else
result
end
end
_ ->
defp chain_type_fields(result, _block, _single_block?) do
result
end
end
end
end

@ -299,4 +299,83 @@ defmodule Explorer.Chain.Block do
end
def uncle_reward_coef, do: @uncle_reward_coef
@doc """
Calculates the gas target for a given block.
The gas target represents the percentage by which the actual gas used is above or below the gas target for the block, adjusted by the elasticity multiplier.
If the `gas_limit` is greater than 0, it calculates the ratio of `gas_used` to `gas_limit` adjusted by this multiplier.
"""
@spec gas_target(t()) :: float()
def gas_target(block) do
if Decimal.compare(block.gas_limit, 0) == :gt do
elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier)
ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier))
ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float()
else
0.0
end
end
@doc """
Calculates the percentage of gas used for a given block relative to its gas limit.
This function determines what percentage of the block's gas limit was actually used by the transactions in the block.
"""
@spec gas_used_percentage(t()) :: float()
def gas_used_percentage(block) do
if Decimal.compare(block.gas_limit, 0) == :gt do
block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float()
else
0.0
end
end
@doc """
Calculates the base fee for the next block based on the current block's gas usage.
The base fee calculation uses the following [formula](https://eips.ethereum.org/EIPS/eip-1559):
gas_target = gas_limit / elasticity_multiplier
base_fee_for_next_block = base_fee_per_gas + (base_fee_per_gas * gas_used_delta / gas_target / base_fee_max_change_denominator)
where elasticity_multiplier is an env variable `EIP_1559_ELASTICITY_MULTIPLIER`,
`gas_used_delta` is the difference between the actual gas used and the target gas
and `base_fee_max_change_denominator` is an env variable `EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR` that limits the maximum change of the base fee from one block to the next.
"""
@spec next_block_base_fee :: Decimal.t() | nil
def next_block_base_fee do
query =
from(block in Block,
where: block.consensus == true,
order_by: [desc: block.number],
limit: 1
)
case Repo.one(query) do
nil -> nil
block -> next_block_base_fee(block)
end
end
@spec next_block_base_fee(t()) :: Decimal.t() | nil
def next_block_base_fee(block) do
elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier)
base_fee_max_change_denominator = Application.get_env(:explorer, :base_fee_max_change_denominator)
gas_target = Decimal.div(block.gas_limit, elasticity_multiplier)
gas_used_delta = Decimal.sub(block.gas_used, gas_target)
base_fee_per_gas_decimal = block.base_fee_per_gas |> Wei.to(:wei)
base_fee_per_gas_decimal &&
base_fee_per_gas_decimal
|> Decimal.mult(gas_used_delta)
|> Decimal.div(gas_target)
|> Decimal.div(base_fee_max_change_denominator)
|> Decimal.add(base_fee_per_gas_decimal)
end
end

@ -10,8 +10,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
from: 2
]
alias EthereumJSONRPC.Blocks
alias Explorer.Chain.{
Block,
DenormalizationHelper,
@ -72,7 +70,15 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
fast_time: nil | Decimal.t()
}
]}
when gas_price: nil | %{price: float(), time: float(), fiat_price: Decimal.t()}
when gas_price:
nil
| %{
base_fee: Decimal.t() | nil,
priority_fee: Decimal.t() | nil,
price: float(),
time: float(),
fiat_price: Decimal.t()
}
def get_average_gas_price(num_of_blocks, safelow_percentile, average_percentile, fast_percentile) do
safelow_percentile_fraction = safelow_percentile / 100
average_percentile_fraction = average_percentile / 100
@ -272,30 +278,30 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
fast_time: fast_time
} = merge_fees(fees)
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{slow_fee, average_fee, fast_fee} =
{slow_fee, average_fee, fast_fee, base_fee_wei} =
case nil not in [slow_priority_fee_per_gas, average_priority_fee_per_gas, fast_priority_fee_per_gas] &&
EthereumJSONRPC.fetch_block_by_tag("pending", json_rpc_named_arguments) do
{:ok, %Blocks{blocks_params: [%{base_fee_per_gas: base_fee}]}} when not is_nil(base_fee) ->
base_fee_wei = base_fee |> Decimal.new() |> Wei.from(:wei)
Block.next_block_base_fee() do
%Decimal{} = base_fee ->
base_fee_wei = base_fee |> Wei.from(:wei)
{
priority_with_base_fee(slow_priority_fee_per_gas, base_fee_wei),
priority_with_base_fee(average_priority_fee_per_gas, base_fee_wei),
priority_with_base_fee(fast_priority_fee_per_gas, base_fee_wei)
priority_with_base_fee(fast_priority_fee_per_gas, base_fee_wei),
base_fee_wei
}
_ ->
{gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price)}
{gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price), nil}
end
exchange_rate_from_db = Market.get_coin_exchange_rate()
%{
slow: compose_gas_price(slow_fee, slow_time, exchange_rate_from_db),
average: compose_gas_price(average_fee, average_time, exchange_rate_from_db),
fast: compose_gas_price(fast_fee, fast_time, exchange_rate_from_db)
slow: compose_gas_price(slow_fee, slow_time, exchange_rate_from_db, base_fee_wei, slow_priority_fee_per_gas),
average:
compose_gas_price(average_fee, average_time, exchange_rate_from_db, base_fee_wei, average_priority_fee_per_gas),
fast: compose_gas_price(fast_fee, fast_time, exchange_rate_from_db, base_fee_wei, fast_priority_fee_per_gas)
}
end
@ -321,11 +327,13 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
end)
end
defp compose_gas_price(fee, time, exchange_rate_from_db) do
defp compose_gas_price(fee, time, exchange_rate_from_db, base_fee, priority_fee) do
%{
price: fee |> format_wei(),
time: time && time |> Decimal.to_float(),
fiat_price: fiat_fee(fee, exchange_rate_from_db)
fiat_price: fiat_fee(fee, exchange_rate_from_db),
base_fee: base_fee |> format_wei(),
priority_fee: base_fee && priority_fee && priority_fee |> Decimal.new() |> Wei.from(:wei) |> format_wei()
}
end
@ -346,6 +354,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
value |> Wei.from(:wei)
end
defp format_wei(nil), do: nil
defp format_wei(wei), do: wei |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2)
defp global_ttl, do: Application.get_env(:explorer, __MODULE__)[:global_ttl]

@ -100,4 +100,44 @@ defmodule Explorer.Chain.BlockTest do
assert %{uncle_reward: ^expected_uncle_reward} = Block.block_reward_by_parts(block, [])
end
end
describe "next_block_base_fee" do
test "with no blocks in the database returns nil" do
assert Block.next_block_base_fee() == nil
end
test "ignores non consensus blocks" do
insert(:block, consensus: false, base_fee_per_gas: Wei.from(Decimal.new(1), :wei))
assert Block.next_block_base_fee() == nil
end
test "returns the next block base fee" do
insert(:block,
consensus: true,
base_fee_per_gas: Wei.from(Decimal.new(1000), :wei),
gas_limit: Decimal.new(30_000_000),
gas_used: Decimal.new(15_000_000)
)
assert Block.next_block_base_fee() == Decimal.new(1000)
insert(:block,
consensus: true,
base_fee_per_gas: Wei.from(Decimal.new(1000), :wei),
gas_limit: Decimal.new(30_000_000),
gas_used: Decimal.new(3_000_000)
)
assert Block.next_block_base_fee() == Decimal.new(900)
insert(:block,
consensus: true,
base_fee_per_gas: Wei.from(Decimal.new(1000), :wei),
gas_limit: Decimal.new(30_000_000),
gas_used: Decimal.new(27_000_000)
)
assert Block.next_block_base_fee() == Decimal.new(1100)
end
end
end

@ -1,54 +1,12 @@
defmodule Explorer.Chain.Cache.GasPriceOracleTest do
use Explorer.DataCase
import Mox
alias Explorer.Chain.Cache.GasPriceOracle
alias Explorer.Chain.Wei
alias Explorer.Counters.AverageBlockTime
@block %{
"difficulty" => "0x0",
"gasLimit" => "0x0",
"gasUsed" => "0x0",
"hash" => "0x29c850324e357f3c0c836d79860c5af55f7b651e5d7ee253c1af1b14908af49c",
"extraData" => "0x0",
"logsBloom" => "0x0",
"miner" => "0x0",
"number" => "0x1",
"parentHash" => "0x0",
"receiptsRoot" => "0x0",
"size" => "0x0",
"sha3Uncles" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x0",
"baseFeePerGas" => "0x1DCD6500",
"totalDifficulty" => "0x0",
"transactions" => [
%{
"blockHash" => "0x29c850324e357f3c0c836d79860c5af55f7b651e5d7ee253c1af1b14908af49c",
"blockNumber" => "0x1",
"from" => "0x0",
"gas" => "0x0",
"gasPrice" => "0x0",
"hash" => "0xa2e81bb56b55ba3dab2daf76501b50dfaad240cccb905dbf89d65c7a84a4a48e",
"input" => "0x",
"nonce" => "0x0",
"r" => "0x0",
"s" => "0x0",
"to" => "0x0",
"transactionIndex" => "0x0",
"v" => "0x0",
"value" => "0x0"
}
],
"transactionsRoot" => "0x0",
"uncles" => []
}
describe "get_average_gas_price/4" do
test "returns nil percentile values if no blocks in the DB" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
assert {{:ok,
%{
slow: nil,
@ -58,8 +16,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns nil percentile values if blocks are empty in the DB" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
insert(:block)
insert(:block)
insert(:block)
@ -73,8 +29,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns nil percentile values for blocks with failed txs in the DB" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
:transaction
@ -99,8 +53,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns nil percentile values for transactions with 0 gas price aka 'whitelisted transactions' in the DB" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729")
@ -137,8 +89,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns the same percentile values if gas price is the same over transactions" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729")
@ -175,8 +125,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns correct min gas price from the block" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729")
@ -225,8 +173,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns correct average percentile" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729")
block3 = insert(:block, number: 102, hash: "0x659b2a1cc4dd1a5729900cf0c81c471d1c7891b2517bf9466f7fba56ead2fca0")
@ -274,10 +220,16 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "returns correct gas price for EIP-1559 transactions" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391")
block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729")
block2 =
insert(:block,
number: 101,
hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729",
gas_limit: Decimal.new(10_000_000),
gas_used: Decimal.new(5_000_000),
base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei)
)
:transaction
|> insert(
@ -330,8 +282,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "return gas prices with time if available" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
block1 =
insert(:block,
number: 100,
@ -343,7 +293,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
insert(:block,
number: 101,
hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729",
timestamp: ~U[2023-12-12 12:13:00.000000Z]
timestamp: ~U[2023-12-12 12:13:00.000000Z],
gas_limit: Decimal.new(10_000_000),
gas_used: Decimal.new(5_000_000),
base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei)
)
:transaction
@ -405,7 +358,6 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
end
test "return gas prices with average block time if earliest_processing_start is not available" do
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
old_env = Application.get_env(:explorer, AverageBlockTime)
Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000)
start_supervised!(AverageBlockTime)
@ -432,7 +384,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
insert(:block,
number: block_number + 103,
consensus: true,
timestamp: Timex.shift(first_timestamp, seconds: -7)
timestamp: Timex.shift(first_timestamp, seconds: -7),
gas_limit: Decimal.new(10_000_000),
gas_used: Decimal.new(5_000_000),
base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei)
)
AverageBlockTime.refresh()

@ -206,7 +206,8 @@ config :explorer,
restricted_list: System.get_env("RESTRICTED_LIST"),
restricted_list_key: System.get_env("RESTRICTED_LIST_KEY"),
checksum_function: checksum_function && String.to_atom(checksum_function),
elasticity_multiplier: ConfigHelper.parse_integer_env_var("EIP_1559_ELASTICITY_MULTIPLIER", 2)
elasticity_multiplier: ConfigHelper.parse_integer_env_var("EIP_1559_ELASTICITY_MULTIPLIER", 2),
base_fee_max_change_denominator: ConfigHelper.parse_integer_env_var("EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR", 8)
config :explorer, :proxy,
caching_implementation_data_enabled: true,

Loading…
Cancel
Save