Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIMD-110: Exponential fee for write lock accounts #110

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
154 changes: 154 additions & 0 deletions proposals/0110-exponential-fee-for-write-lock-accounts.md
@@ -0,0 +1,154 @@
---
simd: '0110'
title: Exponential fee for write lock accounts
authors:
- Anatoly Yakovenko, Tao Zhu
category: Standard
type: Core
status: Draft
created: 2024-01-18
feature: (fill in with feature tracking issues once accepted)
supersedes: (optional - fill this in if the SIMD supersedes a previous SIMD)
extends: (optional - fill this in if the SIMD extends the design of a previous
SIMD)
---

## Summary

In a permissionless environment with low fees, users often submit transactions
for opportunistic trades without considering the success probability. Almost
half of the Compute Units (CUs) in a block are allocated to failed transactions,
leading to undesirable scenario where large portion of compute powers primarily
serving failed DeFi arbitrage transactions. To address this, the proposed
feature introduces economic back pressure, discouraging spam activities and
ensuring efficient network resource utilization. It suggests tracking the
Exponential Moving Average (EMA) of contentious accounts' CU utilization
per block and exponentially increasing the cost to write-lock these accounts
if their utilization remains high.

## Motivation

The motivation behind this feature is to introduce economic back pressure,
dissuading DeFi spammers from overwhelming the network. DeFi spam, defined as
opportunistic trades with a positive return on investment, currently occupies
almost half of the CUs in a block as failed transactions. Economic back
pressure aims to create a deterrent for such spam activities, ensuring network
resources are efficiently utilized and preventing continuous block congestion
caused by failed DeFi spam transactions.

## Alternatives Considered

While the priority fee serves to mitigate low-cost spams by decreasing the
likelihood of less prioritized transactions being included, it cannot entirely
eliminate the inclusion of spam transactions in a block. As long as there
remains a chance, no matter how small, to inexpensively include transactions,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty big assumption. I'd like to see the tx scheduler improvements land in 1.18 before this gets seriously considered. Given how the tx scheduler is currently implemented, it's hard to say priority fees aren't sufficient to adjudicate access to blockspace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am confident improved scheduler in 1.18 will respect priority fee much better, but there is still chance, hight be very small, that lower prio tx lands before higher one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the probability of low-prio txs getting included is low. Then it seems like the EMA raising fees will mainly affect the non-spam devs/users who will now see raised fees. If non-spam is anywhere close to 6M CUs/block, then non-spam users pay and low-priority spammers will only get hit by high fees very rarely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user will pay more when capacity is reduced; once this is reliably predictive, spammer will have to back off; and normal users pay resources per demand.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crispheaney steady state ingress load on leaders is 50kpps+. The pipeline to dedup/sigverify/fee check, has to handle all that load before it gets to the scheduler. If it takes more then 400ms, tx isn't getting prioritized in that block. The only way to get spammers to send fewer txs is to raise the base layer fee.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-01-23 at 9 18 31 PM

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if txs take too long to get through the pipeline, prioritization is not effective. Does that make sense? Doesn't matter what the scheduler does, if it can't see txs because they are still in the queues. we need to force sender to stop economically

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems reasonable we focus on improving the throughput scheduling and pre-scheduling stages so that it's more likely txs are able to be prioritized.

If the chance of inclusion for spam is low, then I'm not convinced they will back off.
Fee-payer is can be totally separate from the account funding economic activity for arbitrage.
I can keep my fee-payer balance low and continue to spam. If the fees get too high for my account to fund, then oh well my tx won't make it into the block because the leader won't include me since I can't pay fees.

the incentive for spamming persists. The proposed feature recognizes that the
current mechanisms have limitations in fully addressing the spam issue, and
thus, seeks to introduce a more robust solution to discourage opportunistic
trades and ensure a more secure and efficient network environment.

## New Terminology

- *compute-unit utilization*: denominated in `cu`, it represents total
compute-units applied to a given resource.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the utilization count both read and write transactions, or just write transactions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just write lock in this proposal, it is possible to implement similar fee to read locks, but rather do that after found success of write lock fee.

- *cost rate*: denominated in `lamport/cu`, it represents the cost per
compute-unit at a given condition.
- *compute unit pricer*: a component tracks Exponential Moving Average of
*compute-unit utilization*, applies a pricing algorithm to provide current
*cost rate*.
- *write lock fee*: denominated in `lamport`, it is fee dedicated for write
lock an account, calculated as `compute-unit-pricer.cost-rate() * transaction.requested_cu()`.

## Detailed Design

### Design Highlights

- Account Association with Compute Unit Pricer:
- Accounts are associated with a *compute unit pricer*, and the *runtime*
maintains an LRU cache of actively contentious accounts' public keys and
their *compute unit pricers*.
Comment on lines +67 to +69

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice way to avoid adding new field to AccountInfo!

- Alternatively, each account can have its *compute unit pricer* stored
onchain, which would require modifying accounts.
Comment on lines +70 to +71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

....but if there are spare bytes, this can be done cheaply (and you can avoid the cache and have a price on literally every account) with two u64s: store last_slot_touched and an int representing log(fee rate) / log(1.01)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require storage forever in the state. An LRU cache should be sufficient.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only if it is big enough!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it only needs to be 2*worst case evictions per block

- Compute Unit Pricer:
- Tracks an account's EMA *compute-unit utilization*, updated after the
current bank is frozen.
- Provides the current *cost rate* when queried.
- EMA of Compute-Unit Utilization:
- Uses 8 slots for EMA calculation.
- EMA Alpha, representing the degree of weighting decrease, is calculated as
`alpha = 2 / (N+1)`.
- Pricing Algorithm:
- Adjusts write-lock *cost rate* based on an account's EMA *compute-unit
utilization*.
- For each block, if an account's EMA *compute-unit utilization* is more than

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a discrete change at 50%? why not increase_pct = f(utilization) where f(0%) = -max, f(100%) = +max (perhaps max = 1%) with a continuous function (e.g. linear)?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and if it's gonna be discrete, why not e.g. 33%?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the shape of the controller deserves some study. understood there is a desire for tx senders to know what the max they might pay is. price as an increasing function of utilization (which all these proposals provide) is already much better than status quo, but a significantly more aggressive increase would help with discrete opportunities like NFT mints and one-time arbs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea. I agree. I think it's worth picking something reasonable and then adjusting it in the future. 1% per block increase puts the worst case on a tx to 4.4x

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discrete makes it easier to manage a cache. Accounts that cross 6m evict the cheapest from the cache.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to @eugene-chen's comment: the choice of 50% implies some target ("utilization <= 50% is fine; over 50% is undesirable"). Since the EMA is tracked anyway, could you have inc_pct = f(utilization) be something like constant * (utilization - target) ?

Since the increase/decrease is in percentage, implicitly the calculation is being done in "logarithmic space" so f(utilization) = exp(constant * (utilization - target) ) is more appropriate. (We have some work that suggests that the current price should probably appear in the exponent as well: see appendix c of this paper.)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discrete is a better devX for clients to figure out pricing

target utilization, its write-lock *cost rate* increases by X%. If it's
below target, the *cost rate* decreases by X%.
- For V0:
- target utilization is `25%` of Max limit;
- Initial write-lock cost rate is `1000 micro-lamport/CU`;
- cost change rate set to 1%;
- Calculate *Write Lock Fee*:
- Fee required to write-lock an account is calculated by multiplying the
write-lock *cost rate* by the transaction's requested CU.

### Detailed Design

- Initialization and Inheritance:
- Bank initializes an empty account_write_lock_fee_cache, an Cache of
{account_pubkey, compute-unit-pricer}.
- Child banks inherit the parent's cache.
- Transaction Fee Calculation:
- Calculate write-lock fee for each account a transaction needs to write,
summing up to be its *write lock fee*. This, along with signature fee and
priority fee, constitutes the total fee for the transaction.
- Leader checks fee payer's balance before scheduling the transaction.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prio fee today and this proposed fee are priced per CU requested. if desired, could also price per CU used, by beginning the tx by cost rate * CU requested and ending the tx by rebating cost rate * (CU requested - CU used)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No rebates please. We need devs to correctly estimate what they are using, not request max.

Copy link

@siong1987 siong1987 Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly, rebate will just make all transactions setting the CU limit to max.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary for the proposal, even if it is how the labs client will do it; scheduling is outside consensus and doesn't affect the fee.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaders should drop invalid fee payers as early as possible though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah definitely, but that's not related to this proposal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is assumed leader will do fee payer check upfront, but if it doesn't (for whatever reason), invalid fee payer will be dropped during loading (after locking). That's make it less effective, but still improvement.

- Cost Tracking:
- Cost_tracker tracks CUs for the current block and each write-locked account
as-is;
- Ensuring cost tracking is enabled at the replay stage.
- End of Block Processing:
- Identify write-locked accounts with *compute-unit utilization* > target
utilization. Add/update bank's account_write_lock_fee_cache.
- For those accounts are in cache, eg were saturated, but not being
write-locked in this block, their current block *compute-unit utilization*
shall be considered as zero; then use it to update Cache.
- Evict cheapest account before add new "hot" accounts into LRU cach, if cache
is in full capacity;
- Account will be evicted from cache as soon as its *cost rate* drop back to 0
- Cache has capacity set to 2* worst case eviction per block to prevent
cache attack.
- For v0, cache capacity set to 2048, as:
- Max number of tansactions with account 6M CU = 48M/6M = 8;
- Max number of accounts per tx: 128;
- worst case per block: 128 * 8 = 1024;
- 2 times worst case: 2048;
- Fee Handling:
- Collected write-lock fees are 100% burnt.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't read locks considered here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other Considerarion section acknowledges read-lock contention and possibility of applying same EMA mechanism. Prefer doing that after seeing success on write-lock first.

- Collected priority fees are 100% rewarded.
- Collected signature fees are 50% burnt, 50% rewarded.

### Other Considerations

- Users may need new instruction to set a maximum write-lock fee for transaction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v0, 1% increase means that worst case is 4.4x increase over 150 slots for a block hash. That should be good enough for wallets to show to users as an estimate

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would a maximum write-lock fee instruction work? at what point does the tx get rejected?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would imagine it would be a max fee as part of the budget program.

I would rather put this complexity outside of the runtime. Runtime needs a cheap fast priority, the non priority fees can be estimated by the wallets off chain.

- Consider tooling for wallets/simulators to query "min/max write lock fee."
- Acknowledge read lock contention, deferring EMA fee implementation for read locks.
- In the future, a percentage of collected write-lock-fee could be deposited
to an account, allowing dApps to refund cranks and other service providers.
This decision should be done via a governance vote.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why governance? governance by whom? what changes belong to governance and what changes belong to e.g. SIMD process?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stake weighed median signaled by validators. Would need a simd in the future

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I will argue in that future SIMD instead :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think some program account deposit is necessary to prevent write lock spamming oracles and things like that. But that can be turn on in v2


## Impact

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one addition: if fee is paid per CU requested, this additionally incentivizes accurate CU estimation beyond what the prio fee already does

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another addition (less obvious if good or bad): this increases the incentive for app developers to fit more of app state into a single account rather than having a bunch of accounts

and incentivizes some type of account-sybiling, e.g. the "canonical account for some app state" rotates every N slots to allow the fee to cool down

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it have the opposite effect? If that account is saturated then fees increase super-linearly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(assuming this is referring to the first note in the second comment) Today a bunch of programs require always hitting accounts A B and C so the user would have to pay for 3 account writes whose price escalate together. So there's an incentive for the developer to compress the state to a single account A to decrease write-lock fee by 2/3

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taozhu-chicago might make sense to scale it by bytes. So there is little advantage to combining accounts.

So each account starts at a lamports per CU per byte rate (LCBR) that scales 1.01x each block.

One way to fit this into existing fee model is to

  • lower the existing signature fee by 50%
  • have validators earn 100% of the signature fee
  • set the floor LCBR to match what votes currently use, so whatever vote writes to be 50% of existing signature fee. Burn 100%

So basically math works out to be the same for votes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

account bytes are converted into CU by cost model, feel it's simpler to stay with "lamports_per_cu". As for vote, it has const CU cost, it can also have const fee, assume it is "simple vote transaction", and all "complex vote transactions" are dropped.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complex votes can pay the complex fee :), they can be dropped by leaders, but i don't think we need to make them invalid in the runtime for this change.


- Rate of successful CU inclusion in a block is expected to increase, reducing
failed transactions.
- Transactions writing to contentious accounts will experience increased fees,
particularly during congestion.
- DeFi arbitrage traders will need to adjust strategies to account for the
heightened fees.

## Security Considerations

none
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can potentially slow the leader by spamming it with transactions using 256 accounts in an ALT all write locked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this isn't unique to this proposal tho. ALT needs to be loaded anyway, the proposal adds LRU cache lookup

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max account locks is only 64. Scheduler should use locks in the equation however

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature activation pending right?
9LZdXeKGeBV6hRLdxS1rHbHoEUsKqesCC2ZAPTPKJAbK | inactive | NA | increase tx account lock limit to 128 #27241

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to feature schedule, it is blocked atm.


## Backwards Compatibility

Needs feature gate.