CAP-0055: Fee model in smart contracts

309 views
Skip to first unread message

Nicolas Barry

unread,
Jun 22, 2022, 12:47:36 PM6/22/22
to Stellar Developers
Hello!


This CAP describes the overall approach to fee modeling in the upcoming smart contract subsystem in core.

This CAP does not cover classic. What we will do is when we're happy with a model for smart contracts, we'll update classic (in a different CAP) to reconcile some aspects while maintaining priorities for classic.

We intentionally published it with many open issues, focusing not on "syntax" (xdr, or even specifics of some algorithms) but on the "why" as there are many new things in there that the existing network does not support (like storing large binary blobs on chain).

A few examples of topics I would like to discuss:
* transaction capacity model wrt classic transactions (fully isolated like in the CAP or not)
* how many markets? (proposal has 2, might be possible to go down to 1 but there are tradeoffs in doing so)
* ledge space fee model: price curve + state expiration

Notable topics partially covered in the CAP but that may belong in other CAPs (or are already partially covered in other CAPs, and need to be reconciled):
* network wide settings
* removing "TransactionResult" from historical data
* events and meta data proofs
* ephemeral payload (included now as I want to avoid updating the tx envelope several times)
* "preflight" endpoint

Nicolas

Leigh McCulloch

unread,
Jun 22, 2022, 12:54:08 PM6/22/22
to Nicolas Barry, Stellar Developers, Graydon Hoare
Hi Nicolas,

ledger entries get an expirationLedger entry that represents when the entry will be considered in "default".

Who chooses the initial expiration date?

What fees are associated with extending the life of a ledger entry?

Bumping the expiration time of a ledger entry
 • should not be restricted in any way: it should be possible for users of a "shared contract" to bump that contract if they find it useful.

This takes some control away from the contract author. I think it would be more consistent with CAP-54 if the host function could only be called by the contract, and then the contract provided a function that would use the host function. The contract author would put whatever authorization they wanted on the function. A common pattern would be to put no authorization and then anyone could bump it. I'm not sure how practically meaningful this is yet, it just seems more consistent with CAP-54.

In the context of this CAP, the only thing that matters is that "gas" represents an arbitrary base unit for "execution time".

Will we also assign gas representing some other dimensions of resource cost, such as memory usage or cpu load? For example if there's a host function that has a small execution time but much higher memory cost that limits the ability to parallelize it, would the gas for calling that function be greater?

Reads are logically performed before transaction execution.

Does this mean that reads are performed before the transaction execution phase of all transactions, or of a single transaction?

Writes are performed after transaction execution

I think we already planned for this, but this would indicate that if we currently rely on any validation to occur within XDR serialization, we might allow a transaction to run for longer than it should rather than failing earlier. We should do validation as data is set by the contract rather than during XDR/ledger serialization. (The Rust stellar-xdr crate does validation when you set values into the object, so I think we're okay on this front, assuming the host VM uses the XDR types at that point.) cc @graydon

The number of bytes to write is the number of bytes associated with bucket entries referenced by the readWrite footprint.
The number of ledger entry to write is the size of the read/write footprint.

Do ledger entries get written even if they aren't changed? Does the user pay for writes even if the contract doesn't change the loaded ledger entries? For example, I can imagine writing a contract function that may need to alter a ledger entry, but may not 90% of the time. Ideally the contract only pays the cost to read it and not write it that 90% of the time. Or is this impractical?

Cheers,
Leigh

--
You received this message because you are subscribed to the Google Groups "Stellar Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to stellar-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/99b5571c-457a-4631-8adc-1f46cd2ace8cn%40googlegroups.com.

Nicolas Barry

unread,
Jul 13, 2022, 8:32:05 PM7/13/22
to Stellar Developers
Bumping this thread with an update on where we are: we've discussed quite a few things in Discord and protocol meetings.

Leigh: I did not answer some of your questions because they're "one or more level down" from those top level questions that we need to address first.

Please anyone: let me know if you have more questions that we should cover at our next protocol meeting.
I am trying to recap as much as possible where we are on key topics.

Ledger State Expiration
This is one of the key decisions we need to make.
Right now the "top contender" is to *not* delete state as explained in the CAP but instead implement a version of "state archiving" (so that tokens do not get "burned").
Relevant conversation was kicked off by DM a few days ago https://groups.google.com/g/stellar-dev/c/MBZq-K-ObFk/m/EuBiIgLoAAAJ

From the CAP's perspective this would not require other changes to the fee policy as space gets freed when items get archived.
In particular, it means that there does not need to be additional incentives to delete data as the policy is reversed (keep paying to stay in the active ledger).

"Ledger Rent"
The alternate approach to on how to charge for "rent" in the CAP seems to be preferred by people.
The currently proposed approach is to extend the "expiration time" using a special operation/host function. As a consequence this requires submitting transactions to refresh ledger entries at least once per "period". This is similar to having to remember to mail your rent check every month.
The alternate approach is to instead attach an XLM balance to each ledger entries, and let the network take what it needs to extend expiration. This is similar to have "auto pay" from an escrow account to pay for rent.
The main advantage of that later approach is that, as a user, I can decide to allocate a balance large enough to cover the cost of storage for potentially years.
There is a bit more complexity in the protocol to make this approach work, but it seems that the user experience is vastly superior, so the trade-off is probably worth considering.

Ledger data write fee
In the CAP, the formula used to derive the "base rate" for writes is expressed using an exponential.
This is actually not needed in practical terms: as we're using a piecewise (2-parts) function, the exponential part can be replaced by another (simpler to calculate) function, like a quadratic function (possibly linear, with a very aggressive slope). The only thing that matters here is that as the ledger gets closer to filling up the "buffer space" the rate just needs to reach prices that no actor should be able to pay.

TransactionResult is meta
As part of CAP-0056 we made the decision to remove the `TransactionResult` from archives (and replace it with a hash), and in the context of this CAP, this means that `TransactionResult` is just another kind of "meta" (called "extended data" in this CAP), which is a lot cheaper. This should translate to lower fees (and less complexity).

Bandwidth
In the CAP network bandwidth was placed in the "flat rate" category, but it turns out that this was too simplistic. In reality: we will have large (potentially very large in the future, depending on what we need to do wrt rollups in particular) payloads. As a consequence, bandwidth has to be treated like other "high contention" resources (compute and ledger space), and as such be subject to "market dynamics".

Multidimensional fee structure
We did not talk about this one enough. The main choice that we need to make here is: do we want to have explicit multiple fee dimensions or not.
The CAP (with the addition of bandwidth), requires 4 fees to be attached to a transaction:
* gas (compute) *dynamic, non refundable*
* bandwidth *dynamic , non refundable *
* ledger data access *dynamic , non refundable *
* other "flat fee rate" (extended data: meta, events, results) *flat fee fixed as per network parameters, refundable*
There is a bunch of work to do to figure out how to prioritize transactions in the context of flooding and crafting transaction sets. I *think* that that work is the same regardless of explicit markets or not (if markets are implicit, it just means we'd do something similar to CAP42).
There might be alternatives to fee markets: it might be possible to turn everything into "flat fee rate", and have the network adjust some parameters dynamically (based on historical data), instead of only being managed by validators. I originally excluded such solutions as I thought they probably are harder to get right or resilient to abuse.

Dmytro Kozhevin

unread,
Jul 28, 2022, 7:38:46 PM7/28/22
to Nicolas Barry, Stellar Developers

Here is my initial proposal on fees & tx set nomination. The focus is on simplicity for the users and adaptability during the ramp up process.


Base fees

Our goals here are to price different ‘market’ resources against each other and to prevent spam similarly to classic.

  • For every ‘market’ resource (i.e. gas, bandwidth, ledger data access) define a base fee per unit as a network parameter

  • Validators upgrade the base fees on a relatively short schedule (at least initially), e.g. once per week

    • Rough idea for adjustment process: define a transaction-level base fee for a maximum capacity transaction as an additional stable network parameter (e.g. 1 XLM). Then adjust per-resource fees proportionally to historical contention during the adjustment period (e.g. if 90% of the time contention was due to gas only, 10% of the time due to network and no contention on ledger data access, then we would set gas base fee to (~0.9  XLM / gas units), network fee to (~0.1 XLM / network units) and ledger data access to some minimum reserve fee, so that everything adds up to 1 XLM)

    • This idea is purely based on the network usage patterns. Alternatively, we could try to figure out market rates for dimensions, but that would require more complex bidding (see below) and the benefit is not immediately apparent.


Bidding

  • Use a single fee bid per for all the ‘market’ resources (‘flat rate’ resources can either be priced implicitly based on their usage)

  • The fee bid effectively declares how much is the user willing to overpay compared to the base cost, with every resource having the same overpayment rate

  • While on paper individual per-dimension fees give more flexibility to the users, it’s both tricky for the user to come up with the right bidding strategy, as well as trickier for Core to build transaction sets using greedy algorithms.

    • We can leave an extension point here and implement this given demand/justification


Surge pricing

  • Every ‘market’ resource is surge-priced separately in CAP-5 style (i.e. if the resource is close to capacity in the transaction set, surge pricing is applied)

    • We could explicitly include the resource base fee in transaction set in CAP-42 style, but that makes transaction set comparison tricky

  • Surge priced resources get charged at the minimum fee rate for the resource, non-surge priced resources get charged at the base fee rate for the resource (recall that the fee rate is the same for all the resources in the same transaction).


Transaction set nomination

  • The first criterion to compare transaction sets is simply sum(fee bid) across all the smart contract transactions. This reflects both having highest bidding transactions and highest capacity utilization.

  • The second criterion can be min/average resource utilization.

  • Transaction set building is out of scope of the CAP, but with the basic greedy methods (e.g. ranking by fee rate or resource tuples) reasonable results can be achieved.


Nicolas Barry

unread,
Oct 19, 2022, 10:04:02 PM10/19/22
to Dmytro Kozhevin, Stellar Developers
Thanks Dima.

Time to pick this back up, it has been a while!

I thought about what you wrote and realized a few things.

it's likely that there will be a lot of contention on compute, ie people competing on "gas". In the last 24h the range has been between 1x and 52000x of the min fee on the public Stellar network.
Raising the minimum fee periodically would reduce the multiplier during surge pricing to a certain extent but this comes at the cost of increasing fees outside of high traffic (which in itself could conflict with the overarching goal of "low cost").
Setting the "higher fees for everyone" problem aside, I am not sure how much it will get reduced, but let's say that during spikes we only get a 100x multiplier.
This means that during those spikes people will have to bid 100x what they would normally bid, across all resources. Applying 100x on compute is probably acceptable (the raised minimum fee for compute would be let's say 0.01 XLM - not super cheap but fairly cheap), but applying that same multiplier on ledger access is going to get very expensive, in particular for transactions that are adding data to the ledger (where the min fee for adding a bunch of data could get close to 1XLM) meaning those transactions end up bidding 101XLM.
* There is the "sticker shock" aspect even if there is no contention on ledger access.
* People would have to be comfortable paying that much in fees in the event that ledger access would also go into surge pricing (which should be unlikely, but possible).

I think that merging all bids into a single bid does not need to be done at the protocol layer: I think your proposal would be the same if SDKs decided to just apply a uniform multiplier on all dimensions, or at least have a maximum multiplier on dimensions that have less contention (this can be reported by the "fee stats" endpoint too).

The "expensive resource" problem causes other problems: like you, I thought that we could just sort transactions by multiplier first, but this biases towards transactions that use cheap resources (that can put high multipliers and still end up with a much lower fee than other transactions).
What that tells me is that to assemble txsets we probably need a different strategy. One possibility could be to produce a few "profiles" (that use different mixes of resources) and perform a simple greedy selection within those profiles (where we can compute a synthetic cost function appropriate for a given profile). Probably worth playing around with a few models.

Nicolas



Dmytro Kozhevin

unread,
Oct 20, 2022, 2:25:40 PM10/20/22
to Nicolas Barry, Stellar Developers
> Raising the minimum fee periodically would reduce the multiplier during surge pricing to a certain extent but this comes at the cost of increasing fees outside of high traffic

The idea here was to cap the max base fee/tx, so while technically txs are more expensive for those who use more expensive resources, the cost is well-defined and bounded by the value we deem to be sane. In general, no matter what is the approach we take, we would need to define the upper bound on the base fee per transaction, i.e. how much do we think it's fair to charge in absence of surge pricing. Whether this bound is high or low is up to debate, but if everything is within this bound, then I'm not sure it's correct to say that fees are 'increased/decreased'.

> (the raised minimum fee for compute would be let's say 0.01 XLM - not super cheap but fairly cheap), but applying that same multiplier on ledger access is going to get very expensive, in particular for transactions that are adding data to the ledger (where the min fee for adding a bunch of data could get close to 1XLM)

I'm not sure where these numbers come from. If we define max base fee/tx as, say, 0.1 XLM, then during 100x surge the max spend would be 10 XLM, no matter what is the actual resource consumption. Basically, if ledger access contention is rare, then with my proposal it would get a low fraction of tx base fee (say, 5%), hence you would get N * 5% additional spend during surges.

> I think that merging all bids into a single bid does not need to be done at the protocol layer

I believe reporting the individual dimension stats in this case would be rather misleading, because everyone's bid is a tuple of 3 bids. So the idea here is to make it simple to reason about the bidding strategy and not to just simplify the transactions (which indeed could be done at SDK level).

> but this biases towards transactions that use cheap resources (that can put high multipliers and still end up with a much lower fee than other transactions).

Is that really a problem? If 99% of the time the contention happens due to gas and someone submits a high fee-rate tx that, say, consumes 0.1% of per-ledger gas limit, but does a lot of writes, then what is the issue with including it into ledger? I don't think we should prioritize any particular resource; the goal should be to execute as many transactions as possible within the limits and I believe it's fair to prioritize low-utilization and high-bid txs (as they don't really cause more contention). 

I believe that combining resource prioritization with individual per-resource bids would lead to more incentive to game the system in more ways (e.g. if we prioritize gas fee-rate first, the txs with low gas consumption and high gas fee still would be prioritized, but unlike in my proposal the bidder may also bid the base fee on 'non-contended' resources). 


Nicolas Barry

unread,
Oct 21, 2022, 8:25:53 PM10/21/22
to Dmytro Kozhevin, Stellar Developers
OK, so I think I found what was wrong, and  I think we're actually making progress. I took most of your ideas and integrated this into this updated version.

`min_LedgerData_fee` in the CAP contains two parts that are entirely different:
* the fee bid related to the footprint, that translates into IOPS
* the storage fee when adding data to the ledger

Only the first one should be "scaled". I think that after that fix, using a shared "multiplier" seems viable to me.

So basically what we end up with is:
* on one side, resources for which transactions compete against each other
    * gas
    * ledger iops
    * network utilization
* on the other, fees that are determined on a per ledger basis (derived from network settings and ledger state). I called them `flatResourcesFee` in the CAP.
    * historical data (archives)
    * extended data (meta, events, etc)
    * ledger storage


For the first type, each transaction has a `min_fee` (derived from actual resource consumption) for each of those resource types.

If we attach like you were saying the multiplier to the transaction to apply on top of the "sum of min_fees".
We end up with [multiplier, [min_fee_1, .., min_fee_N], flatResourcesFees]. Having the multiplier like this allows to avoid problems when comparing transactions (like in classic, we have to compare fractions, which is annoying).

The source account has to be able to pay for `multiplier*"sum of min_fees" + flatResourcesFees`.

then the comparison function that we can use during surge pricing is to compare transactions in the following order:
* multiplier descending
* "sum of min_fees" descending
* other non fee related tie breaker (cannot use "flatResourcesFee" as those get refunded)

If we do that, I also think that a greedy algorithm like what we do now would work fine.

When we charge fees at apply time, I don't think we can easily give a discount "CAP 5" style on resources that do not surge price as transactions are sorted by "sum of min_fees", so my inclination would be to not support the equivalent of `TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE` with a `baseFee` set for Soroban transactions.

It's conceivable that we could do some sort of multi pass algo where "sum of min_fees" is only for resources that are surge pricing (or maybe first compare "sum of min fees surging" and then "sum of min fees not surging"), so the nomination algo would have to perform up to N (number of resources) passes; but I am not sure it would work that much better and it makes it harder to manage at the overlay layer (transaction queues).

The thing that is still a bit in the air is the requirement for network operators to agree on min fee parameters updates on a regular basis. I think it would be useful to quantify the "badness" with a few examples. Ideally operators would not have to be that involved in the thing and only coordinate when certain thresholds are crossed.


Dmytro Kozhevin

unread,
Oct 24, 2022, 2:30:23 PM10/24/22
to Nicolas Barry, Stellar Developers
That sounds good to me, besides some open questions:

> so my inclination would be to not support the equivalent of `TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE` with a `baseFee` set for Soroban transactions.

I suppose you meant *to support*. I'm not opposed to that, but we need some tx set comparison criterion that takes discounts into account.

> Ideally operators would not have to be that involved in the thing and only coordinate when certain thresholds are crossed.

If we can set up automated updates, then only the upgrade schedule would need to be agreed on (the nominated upgrade set should look the same, as long as the same ledger range is taken into account and the same algorithm is used). I think after the initial calibration upgrades could switch to on-demand basis based off, e.g. MSE between current and computed proportions (we could monitor this or recompute manually every once in a while). Of course, some tools need to be built for that, but I'm not sure it's possible to come up with more autonomous solution.

Reply all
Reply to author
Forward
0 new messages