fip | title | author | discussions-to | status | type | category (*only required for Standard Track) | created | spec-sections | requires | ||
---|---|---|---|---|---|---|---|---|---|---|---|
0057 |
Update gas charging schedule and system limits for FEVM |
Steven Allen (@stebalien), Raúl Kripalani (@raulk), Akosh Farkash (@aakoshh), Jakub Sztandera (@Kubuxu) |
Final |
Technical Core |
Core |
2022-12-12 |
|
0032, PR-512 (draft) |
With the introduction of user programmable smart contracts, we need to ensure that the gas model accurately reflects the cost of on-chain computation. In the current network, users can only trigger course-grained behaviors by submitting messages to built-in actors. However, with the introduction of FEVM, users will be able to deploy new smart contracts with fine-grained control over FVM execution. If we don't carefully tune the gas model to accurately charge for computation, an attacker could carefully construct a contract to take advantage that and slow down or even stop chain validation.
This FIP proposes a few adjustments to the gas charging schedule. Specifically:
- Adjustments to some syscall gas:
- State read and write costs.
- Internal send costs.
- Hashing, signature verification, and randomness.
- Variable charges for memory initialization, copying, and allocation.
- New charges for state-tree reads and writes.
- New charges for actor address resolution.
- Increase the state storage gas cost from 1,300 to 3,340 gas.
This FIP also introduces overall memory limits. However, these memory limits currently only include Wasm memories and table elements (not IPLD blocks, wasm code, and other metadata).
Additionally, this FIP introduces a maximum block size limit of 1MiB for all newly created IPLD blocks.
Since the above changes the estimated gas usage of PreCommit and ProveCommit messages, this FIP introduces a recalibration of the corresponding parameters in the Batch Balancer mechanism.
Finally, this FIP reduces the maximum recursive call depth limit from 1025 to 1024 to bring it in-line with the initial intentions of this limit and other blockchain VMs.
With the introduction of FEVM, users will be able to trigger operations that were previously priced according to "expected costs". These prices took things like caching, overall message execution costs, and size limits imposed by the actors themselves into account. Unfortunately, with the introduction of FEVM, these assumptions no longer hold.
With a FEVM contract, users can:
- Perform operations that have runtime cost varying in the size of the inputs (bulk memory copies, hashing, etc.). These operations previously had fixed costs.
- Make state read and write operations that have not been re-priced since network launch. These prices were estimated before the FVM existed, and while the state-tree was quite small.
- Make syscalls that were previously expected to have their costs amortized over the execution of the transaction as a whole. Unfortunately, with FEVM, prices must assume random pathological access.
- Allocate arbitrary amounts of memory. Existing built-in actors have explicit limits to avoid running out of memory, but FEVM requires system-imposed global memory limits.
- Store arbitrary state, potentially bloating the state-tree.
Notes:
- Filecoin targets 10 gas/ns. That is, an operation that takes 1 nanosecond should cost 10 gas.
- The FVM supports gas charges with milligas precision. That is, we can charge for gas in increments of 0.001 gas.
- In terms of chain throughput (gas per unit time), there's about 444 Filecoin gas to every 1 Ethereum gas. This ratio should be kept in mind when comparing Filecoin gas costs to Ethereum gas costs.
First and foremost, this FIP makes a few adjustments to FIP-0032.
- Memory copy costs
p_memcpy_per_byte
have been reduced from 0.5gas/byte to 0.4gas/byte to more accurately match benchmarks. We're reasonably confident in this number as it exactly matches the expected speed of 3200 MT/s memory. - The explicit "extern" cost (
p_extern_gas
) of 21,000 gas has been rolled into the various syscall costs themselves to make it easier to reason about the costs of the individual syscalls. This change has no affect on the gas charged, it simply makes it easier to benchmark and discuss changes to gas charges.
Filecoin has historically under-priced storage costs. This hasn't been a huge issue because all state was "managed" by built-in actors, but FEVM introduces userspace contracts, which have more direct control over storage.
Specifically, the per-byte storage cost for on-chain state (excluding messages and receipts) is increasing from 1,300 to 3,440 gas/byte.
This FIP adjusts the gas costs for most of the "core" syscalls:
- State-related operations
- Message sending
- Hashing
- Randomness
- Signature verification
This FIP does not adjust gas values for storage-proof related operations as those operations are not relevant to FEVM.
This FIP updates the cost of all 4 IPLD operations according to the latest benchmarks.
- The flat fee has been increased from 135,617 to 187,440 (
p_blockopen_base_gas
) to account for an increased size in the state-tree since network launch. - The per-byte fee has been reduced from
10.5 gas/byte
to a flat10 gas/byte
(p_memret_per_byte
). This reduction comes from the fact that the "memory retention" cost more than covers the actual per-byte compute cost from loading IPLD blocks.
This syscall now charges 187,444 + 10 * block_size
(in addition to p_syscall_gas
).
The per-byte fee has been reduced from 10.5 gas/byte
to a flat 10 gas/byte
for the same reasons as ipld::block_open
.
This syscall now charges 10 * block_size
(in addition to p_syscall_gas
).
The per-byte fee has been reduced from 0.5 gas/byte
to 0.4 gas/byte
due to the adjustment to p_memcpy_per_byte
.
This syscall now charges 0.4 * read_size
(in addition to p_syscall_gas
).
The cost of "linking" a block has been adjusted to take deferred compute costs (the cost of "flushing" blocks at the end of an epoch), hashing, allocating, and copying into account.
Specifically, this FIP proposes the following:
- A
2.4 gas/byte
fee for to allocate and copy the block into temporary storage. - A per-byte hashing fee according to the price list defined in the hashing section below.
- A fixed per-block fee of 172,000 (
p_blocklink_base_gas
) gas to account for the cost of "flushing" blocks at the end of an epoch. - A fixed 334,000 per-block storage fee to account for metadata overhead.
- A per-byte storage fee of 3,340 gas/byte.
These fees supersede the current prices:
- A 1,300 gas/byte storage fee.
- A fixed 353,640 per-block fee.
This syscall now charges 172,000 + 334,000 + (2.4 + per_byte_hash_fee + 3,340) * block_size
(in addition to p_syscall_gas
).
This FIP changes send
costs (both the top-level send and internal sends) to:
- Account for the cost of instantiating and invoking a Wasm actor.
- Not account for account actor state loading and storing as those costs will now be accounted for separately.
Previously, there were four send-related gas fees:
- A base send cost of 29233 gas.
- A transfer charge of 27500 gas.
- A transfer only premium of 159672 gas. This was charged for actors that didn't invoke any actual code.
- A method invocation (actor instantiation) "rebate"' of 5377 gas. This gas was refunded to actors that actually invoked code.
These costs were designed to "make the numbers work out" in aggregate and took things like actor state loading/storing into account.
This model has been simplified to:
- A fixed "transfer" fee of 6,000 (
p_transfer
) gas for all non-zero value transfers. - A fixed "invocation" cost of 75,000 (
p_invocation
) for all sends except method 0, to charge for instantiating and calling into an actor. - Actor state read/write fees as defined below.
Previously, the FVM relied heavily on caching to reduce the costs of actor state loading. Unfortunately, in the presence of user defined actors, it's now possible to trigger frequent and random state reads/writes.
This FIP introduces:
- An actor-state lookup cost of 500,000 gas (
p_actor_lookup
). - An actor-state update cost of 475,000 gas (
p_actor_update
).
As these costs are significant, this FIP proposes two mechanisms to reduce these costs:
- First, this FIP takes caching into account and only charges for the first time an actor is loaded/updated during a single transaction (taking reverts into account).
- Second, this FIP will not charge to load or update the builtin singleton actor states (actors 0-7, 10, and 99) as those actors will be amortized over the execution of the block.
- Third, this FIP will not charge to load the top-level message sender and recipient. The client is expected to preload them.
Specifically:
- The FVM will keep two sets:
state_lookup_set
state_update_set
- Additionally, the FVM will keep a state read cache and a state write cache.
- At the beginning of the epoch, the FVM will "warm" the state read cache by loading the singleton actors specified above.
- Before executing each message, the FVM will re-initialize
state_lookup_set
andstate_update_set
to the set of singleton actors specified above (as if they had already been updated). - Before performing a state read, the FVM will check the
state_lookup_set
set. If the actor ID is not present, the FVM will chargep_actor_lookup
. - Before performing a state write, the FVM will check both
state_lookup_set
andstate_update_set
. If the actor ID is not present, the FVM will charge for a read and/or write, respectively or both. - After performing a state read, the FVM will add the target actor ID
state_lookup_set
(whether or not the actor exists). - After performing a state write, the FVM will add the target actor ID
state_lookup_set
andstate_update_set
. - Finally, on revert, the FVM will "roll back" any changes to the
state_lookup_set
,state_update_set
, and the read/write caches.
The FVM will apply these charges during the following operations:
- The top-level send will charge to update the top-level message sender (due to the nonce update).
- On all sends:
- The FVM will charge to load the receiver's state.
- The FVM will charge to update the sender's state if value is transferred.
- The FVM will charge to update the receiver's state if:
- The receiver is automatically created.
- Value is transferred.
- When creating a new actor, the FVM will charge for updating the new actor's state.
- On
self::self_destruct
, the FVM will charge to update both the current actor and the beneficiary. - Additionally, the FVM will charge for actor reads on the following syscalls:
actor::get_actor_code_cid
actor::balance_of
actor::lookup_delegated_address
- Additionally, the FVM will charge for actor updates on the following syscalls:
self::set_root
As with actor-state lookups, the FVM heavily relies on caching to amortize the cost of address resolution. This FIP proposes a similar mechanism to charge for them.
This FIP introduces an address lookup cost of p_address_lookup
(1,050,000 gas) to be charged the first time an address is successfully resolved while executing a top-level message (taking reverts into account). However:
- The addresses included in the top-level message are assumed to be pre-resolved.
- No charge is applied for resolving
f0
addresses (ID addresses).
Specifically:
- The FVM will keep the set
address_resolution_set
. - Additionally, the FVM will keep an address resolution cache.
- Before executing each message, the FVM will re-initialize
address_resolution_set
to the top-level message's sender/receiver. - Before resolving an address, the FVM will check
address_resolution_set
. If the address to be resolved is not present, the FVM will chargep_address_lookup
. - After successfully resolving an address, the FVM will add the target actor ID
address_resolution_set
. - Finally, on revert, the FVM will "roll back" any changes to
address_resolution_set
.
The FVM will apply these charges during the following operations:
- On internal
send::send
calls when the recipient is not anf0
address. - On calls to
actor::resolve_address
(again, when the target address is not anf0
address).
The generic "actor creation" compute cost of 1,108,454 is removed and the actor creation storage cost is increased from 98,800 gas to 650,000 gas (p_actor_create_storage
).
Whenever a new actor is created via the init actor (i.e., via the actor::create_actor
syscall), we charge for the new actor's state-tree update and storage:
p_actor_update + p_actor_lookup
+ p_actor_create_storage
-----------------------------------------
= 1,625,000
When an actor is implicitly created due to a send, we charge an additional p_address_assignment
(1,000,000) and p_address_lookup
and because, in this case, the FVM assigns addresses directly, bypassing the init actor.
p_actor_update + p_actor_lookup
p_address_assignment + p_address_lookup
+ p_actor_create_storage
-----------------------------------------
= 3,775,000
There is currently a gas refund on actor deletion of 1300*(36+40) = 98,800
. This FIP removes this refund entirely.
Before this FIP, hashing cost a flat 31355 gas. This FIP proposes the following gas fee schedule, incurred when calling the crypto::hash
syscall.
Hash Function | Per-Byte |
---|---|
SHA2-256 | 7 gas |
Blake2b-256 | 10 gas |
Blake2b-512 | 10 gas |
Keccak256 | 33 gas |
Ripemd160 | 35 gas |
Whether this is a net reduction or increase depends on the length of the preimage. For example, for the Blake2b family, the crossover is at ~3135 bytes.
Before this FIP, signature verification had these costs:
- BLS: 16598605 gas.
- secp256k1: 1637292 gas.
This FIP adds a per-byte cost as well:
- 10 gas/byte for secp256k1.
- 26 gas/byte for BLS.
Note: the new crypto::recover_secp_public_key
only charges the base 1637292 gas as it doesn't have to hash any messages.
This implies a slight base upwards revision of existing fees, which should have minimal impact because:
- These operations aren't frequent.
- The base cost dwarfs the cost of hashing.
Previously, retrieving randomness only cost the flat p_extern_gas
fee (FIP-0032). This FIP proposes an additional charge to cover the hashing involved. Specifically, (seed_size + entropy_size) * blake2b_hashing + p_extern_gas
where:
seed_size
is 48 (the size of the randomness "seed").entropy_size
is the size of the entropy passed by the actor (usually 8).blake2b_hashing
is 10 (as defined above in the hashing section.
This should usually come out to about 560 gas which will increase the cost of this operation by about 2% in most cases. However, this additional charge prevents an attacker from potentially tricking the system into hashing a large amount of data for free.
This FIP introduces charges for Wasm instructions that operate on variable-sized memory.
The memory copy cost (p_memcpy_gas_per_byte
) was first introduced in FIP-0032 but only applied to memory copying within the FVM itself.
This FIP:
- Reduces this fee from 500 milligas to 400 milligas. This reduction applies to the relevant syscalls as well (e.g., block reads and writes).
- Charges this fee (per byte) for instructions that operate on variable-sized memory:
{table,memory}.{init,fill,copy,grow}
. Table operations charge for 8 bytes per table entry (3.2 gas/entry). - Charges this fee (again, per byte) for memory initialized when instantiating an actor. This includes both the Wasm memory and Wasm any tables initialized.
- Removes the "one free page of memory" rule from FIP-0032. The FVM now charges for every page of memory allocated, even on initialization.
A call stack is entitled to allocate a maximum of 2GiB in cumulative Wasm memory across all active invocation containers, and 512MiB for any given actor instance.
- If an actor attempts to exceed either of these limits at runtime, the Wasm container will return
-1
to the actor (as per the Wasm spec). All current built-in actors will then abort immediately with theSYS_ILLEGAL_INSTRUCTION
exit code. - If instantiating an actor would exceed either of these limits, the instantiating actor will immediately exit with the
SYS_ILLEGAL_INSTRUCTION
exit code.
This FIP introduces a 1MiB limit on all newly created blocks (through the ipld::block_create
syscall). This affects:
- Blocks in state.
- The size of internal (actor to actor) messages and return values.
The above changes result in PreCommit
messages being expected to consume 5% more gas, while ProveCommit
messages will consume 20% more gas. The gas usage of these messages is used as an input parameter in the batch balancer mechanism, specified in FIP-0024 as SinglePreCommitGasUsage = 16433324.1825
and SingleProveCommitGasUsage = 49299972.5475
To maintain the current thresholds required for batching, in terms of high enough base fees and number of proofs in a batch, these parameters must be updated, relative to their FIP-0024 values. This FIP updates these parameters to,
SinglePreCommitGasUsage = 1.05 * SinglePreCommitGasUsage =
SingleProveCommitGasUsage = 1.20 * SingleProveCommitGasUsage
or,
SinglePreCommitGasUsage = 17254990.3916
SingleProveCommitGasUsage = 59159967.0570
Execution fidelity, accuracy, and security are the overarching principles that motivates this FIP. Fees have been revised to deliver a more accurate gas model while preserving the security properties of the network, which revolve around the baseline cost of 10 gas/ns.
Filecoin has, historically, charged very little for on-chain state storage. Attempts to change this in the past have been met with resistance as doing so would increase gas fees and reduce chain bandwidth.
Specifically, in terms of chain bandwidth, Ethereum storage gas costs ported to Filecoin would be:
- ~120,000 gas for new state.
- ~30,000 gas for state updates.
Calculated as: EVM_GAS_COST/(32+log2(32)) * 444
.
Two things have changed:
- FEVM is introducing user-specified contracts which can arbitrarily bloat the state.
- Optimizations to the built-in actors reducing gas costs by 30% on average.
While we can't increase the storage costs to where they should be with respect to the EVM, we have an opportunity to increase them a bit while leaving the average gas burn the same.
Specifically, by setting the storage gas to 3,340gas/byte, we see a net change of -0.5% gas usage overall. Further details can be found in the product consideration section.
The 2GiB cumulative memory limit was calculated from:
- The default stack size of rust-based actors (1MiB).
- The default maximum call depth (1024).
- Multiplied by 2 for overhead.
The block size limit was chosen to be 1MiB as that should be an order of magnitude larger than any IPLD blocks currently found on-chain (except for the builtin actor wasm bytecode).
Except for bulk memory operations this FIP does not adjust the cost of individual Wasm instructions. The rational is two-fold:
- FEVM does not use some of the more expensive Wasm instructions (SIMD and floating point operations).
- There is enough overhead for each EVM instruction that it's impossible to repeatedly invoke a single expensive Wasm operation.
We specifically verified this for random memory reads and random jumps in in filecoin-project/fvm-bench#6. These MLOAD instructions ended up costing ~50x more gas than we would have charged for a single random memory access (assuming a memory latency of around 10-15ns).
The send, hashing, signature verification, allocation/memcpy, and randomness costs were all determined through benchmarking as well. These benchmarks can be run stand-alone via the calibration test suite.
Notes:
- The sha256 benchmarks assume hardware support.
- The randomness syscall was not directly benchmarked and the gas costs are computed purely based on the cost of hashing.
- All of these calibration benchmarks run in isolation and don't include the true cost of reading/writing state. Those costs were benchmarked separately below.
The new read costs and write costs were determined through benchmarking against lotus starting from a snapshot and reflect the current cost of reading/writing to the datastore given the current state-tree size.
- The read-costs account for both the read latency and the cost of copying newly read data several times from the client into the FVM.
- The write-costs (computation only) account for the expected per-block overhead incurred in the final "flush" operation. The per-byte costs were ignored as the storage cost (1300 gas/byte) vastly exceeds the computational cost of storing these blocks.
Actor state costs were determined by benchmarking.
- Benchmarking the expected number of reads & writes per update/lookup amortized over a "normal" block execution. This led to an expectation of 2.3 blocks/operation.
- Benchmarking the expected amount of data read/written. This lead to ~6KB on average.
From here, we calculated the lookup cost as:
2.3 * p_blockopen_base_gas # Charge for the latency.
+ 6000 * p_memret_per_byte # Charge for caching the state-tree.
------------------------------
= 491,112
=~ 500,000 # Add some fuzz for decoding and security.
We can similarly calculate the write cost as follows:
2.3 * p_blocklink_base_gas # Per block.
6000 * 10 # Charge for blake2b hashing.
6000 * 2 # Charge for allocation (2/byte from benchmarks)
+ 6000 * 0.8 # Charge for an assumed 2 copies to flush.
------------------------------
= 460,400
=~ 475,000 # Round up to account for state churn.
Previously, Filecoin charged for 32+40 = 72
bytes when creating new actors. However, the actor-state object has changed since network launch (and this was always an undercharge anyways). We now calculate this charge as:
36 # Actor State CID
36 # Actor Code CID
8 # Nonce (max)
16 # Balance (max)
+ 64 # Delegated address (max)
-----
= 160
We then round this up to 192 to account for structure and state-tree overhead, which yields 192 * 3,340 = 641,280 ~= 650,000
gas.
This FIP removes the actor deletion refund because:
- It only applies to payment channels (at the moment).
- It's tiny compared to the computation cost of updating the state-tree. In this new gas model, the computational cost of updating the state-tree vastly exceeds the gas refund.
- Negative gas charges are, in general, a security hazard.
In the future, we will likely want some incentive to delete unused actor state. But that incentive will:
- Have to be proportional to the amount of state freed.
- Have to be something other than gas.
Address resolution and assignment costs were derived from the state read costs with some rounding applied to account for computation, state churn, etc.
The benchmarked values were:
- 5.3 (average) blocks read/written per address lookup/assignment.
- 5710 bytes read/written per address lookup/assignment.
From there, we derived the lookup cost (p_address_lookup
) as follows:
5.3 * p_blockopen_base_gas # Per block read.
+ 5710 * 5.7 # Charge derived from the per-byte block read costs.
------------------------------
= 1,025,979
=~ 1,050,000
We derive the address assignment cost as follows (p_address_assignment
):
5.3 * p_blocklink_base_gas # Per block.
5710 * 10 # Charge for blake2b hashing.
5710 * 2 # Charge for allocation (2/byte from benchmarks)
+ 5710 * 0.8 # Charge for an assumed 2 copies to flush.
------------------------------
= 984,688
=~ 1,000,000 # Round up to account for state churn, encoding, etc.
This FIP is consensus breaking, but should have no other impact on backwards compatibility. However, agents making assumptions about gas (e.g. setting hardcoded gas limits) may need to adapt.
TODO: Will be covered by conformance tests.
This FIP exists to improve the security of the Filecoin network, but has intentionally skipped over a few cases:
- This FIP does not charge variable amounts for different types of mathematical operations. For example, in the future, we'll likely want to charge more for floating point operations, and significantly more for the "sqrt" operation.
- This FIP does not make any explicit charges to account for memory latency.
However, the proposed gas schedule should be "good enough" for FEVM because:
- There's enough overhead in FEVM to "drown out" the effects of memory latency.
- Floating point instructions, including the square-root instruction, are not available in the EVM.
As with all gas schedule changes, this FIP introduces changes to the operation costs of built-in actors which may alter operational cost structures for network stakeholders affective incentive structures.
Overall, this FIP changes average gas usage by -0.5% after taking other actor optimizations into account.
However, there are a couple of important cases to note:
- The message inclusion cost increases by 475,000 gas to account for updating the sending actor.
- Any non-trivial message will incur an additional 475,000 gas fee to update the receiving actor.
- Due to these changes, the cost of a simple value transfer from an account increases by ~2.2x.
Furthermore, with respect to PublishStorageDeals
:
- PSD messages with multiple clients will be more expensive than PSD messages with fewer clients as there's a per-client 500k gas charge to lookup the client in question. However, this effect is minimal compared to the overall cost of these messages.
- PSD messages that address clients by f1-f4 addressees will cost more than PSD messages that address said clients by their ID (f0 address) due to the cost of the additional lookup. However, again, the effects of this cost are minimal compared to the overall cost of these messages.
All together, when benchmarking with the new gas numbers, we see the following costs (on average):
Code | Method | Event Count | Existing gas usage | Gas usage with FIP-0057 | % Change |
---|---|---|---|---|---|
fil/9/account | 0 | 1223 | 291660.72 | 645124.08 | 121.18 |
fil/9/multisig | 2 | 40 | 5938757.07 | 8710765.85 | 46.67 |
fil/9/multisig | 3 | 2 | 24906502.50 | 29119872.00 | 16.91 |
fil/9/reward | 2 | 5547 | 40317641.00 | 43873338.19 | 8.81 |
fil/9/storagemarket | 2 | 1650 | 24982007.47 | 23173395.18 | -7.23 |
fil/9/storagemarket | 4 | 11953 | 606009920.31 | 541451416.73 | -10.65 |
fil/9/storageminer | 0 | 7 | 216405.00 | 481000.00 | 122.26 |
fil/9/storageminer | 3 | 1 | 3146882.00 | 6040850.00 | 91.96 |
fil/9/storageminer | 4 | 10 | 2601772.10 | 4474645.00 | 71.98 |
fil/9/storageminer | 5 | 46986 | 52907581.11 | 49981422.43 | -5.53 |
fil/9/storageminer | 6 | 49513 | 46352972.94 | 48949895.13 | 5.60 |
fil/9/storageminer | 7 | 55086 | 63102326.38 | 75481067.70 | 19.61 |
fil/9/storageminer | 8 | 646 | 523704602.86 | 514605025.93 | -1.73 |
fil/9/storageminer | 11 | 406 | 429599995.23 | 232335284.18 | -45.91 |
fil/9/storageminer | 16 | 140 | 27477805.81 | 32683070.28 | 18.94 |
fil/9/storageminer | 18 | 5 | 2571314.40 | 4440263.80 | 72.68 |
fil/9/storageminer | 23 | 6 | 2593594.83 | 4469491.16 | 72.32 |
fil/9/storageminer | 24 | 1 | 4096410066.00 | 2142130910.00 | -47.70 |
fil/9/storageminer | 25 | 1517 | 260234684.68 | 278624611.67 | 7.06 |
fil/9/storageminer | 26 | 510 | 2255779780.47 | 2582969572.70 | 14.50 |
fil/9/storageminer | 27 | 967 | 270460447.84 | 318397920.82 | 17.72 |
In terms of the market actor, PublishStorageDeals is ~10% cheaper.
In terms of the miner actor (storage provider operations):
- Window post messages are ~5.5% cheaper on average.
- Window post disputes (security critical) are ~47% cheaper on average.
- PreCommit is ~5% more expensive.
- ProveCommit is ~20% more expensive.
- PreCommitAggregate is ~7% more expensive.
- ProveCommitAggregate is ~15% more expensive.
- ProveReplica is ~17% more expensive.
These changes have been implemented in the reference implementation of the FVM.
Copyright and related rights waived via CC0.