Gas cost for precompiles

A brief reminder what gas parameters we are using for precompiles and what role they play:

  • intrinsic gas = storage cost * tx size + base gas
    if transaction can’t cover intrinsic gas it is ineffective, won’t be executed and will be dropped from a block during construction.
  • base gas
    fixed amount of gas set for every template. should cover verification cost (in precompiles case 1-3 ed25519 signatures)
  • fixed gas
    cost to execute a transaction. in precompiles all transactions are doing basic arithmetic and loading/storing state.

Resources cost

CPU cost

Single ed25519 verify takes between 20-200 us per edd25519 signature. Mid range cpu would take about 100 us, this is what i will be using as a reference for estimation below.

Every other cpu operation in precompiles is negligible in comparison, such as arithmetic for Spend transactions.

As a reference cpu cost i take 2 cents per hour, based on cheap vcpu on gcp. In an hour single core can run 36000000 ed25519 instructions. So by dividing 2 cents by 36000000 we get:

CPU_PRICE = 5.5e-10 USD

Storage cost

To define byte price i took a price of cheap 1TB ssd device, 50 USD. Assuming that it lasts for 10 years, we also take electricity cost to be approximately 100 USD for that device.

BYTE_PRICE = 150 / 1073741824 = 1.4e-07 USD

USD to smidges

Total issuence is 2_400_000_000_000_000_000 smidges.
I assume random market cap of 600_000_000_000. I think optimistic makes sense, as the total cost can be always scaled with gas price.
For computation below single smidge is 2.5e-07 USD.

Pricing transactions

BASE_GAS = N * CPU_PRICE
N - is a number of ed25519 signatures. Precompiled templates will use between 1-3 signatures.

INTRINSIC_GAS = BYTE_PRICE * TX_SIZE + BASE_GAS

Besides storing transaction itself - every transaction writes something to the state itself. For comparison on ethereum SSTORE costs 20000 gas per 32 bytes, and storing calldata costs 512 gas per 32 bytes. Below i will use a factor of 30 for the difference in price between updating state and storing transactions.

FIXED_GAS = N * (30 * BYTE_PRICE * STATE_UPDATE_SIZE + BYTE_PRICE * LOAD_STATE_SIZE / 10)

STATE_UPDATE_SIZE for spawn transactions will include immutable state + balance + nonce, for spend transactions only balance + nonce.

N in FIXED_GAS is the number of loaded accounts, it will be between 1 and 3 for different transactions types.

Single sig

Spawn transaction size - 150 bytes

INTRINSIC_GAS = 1.4e-07 * 150 + 5.5e-10 = 2.1e-05 USD = 84 smidges.
FIXED_GAS = 30 * 1.4e-07 * 48 = 2e-04 USD = 800 smidges
TOTAL_GAS = 884 smidges

Spend transaction size - 120 bytes.

INTRINSIC_GAS = 1.4e-07 * 120 + 5.5e-10 = 1.68-05 USD = 67 smidges
FIXED_GAS = 30 * 1.4e-07 * 16 + 1.4e-08 * 48 = 6.78e-05 = 271 smidges
TOTAL_GAS = 338

Multi sig 3/5

Spawn transaction size - 410 bytes.

INTRINSIC_GAS = 1.4e-07 * 410 + 5.5e-10 * 3 = 5.74e-05 USD= = 271 smidges
FIXED_GAS = 30 * 1.4e-07 * 176 = 7e-04 = 2800 smidges
TOTAL_GAS = 3071 smidges

Fixed gas includes payment for 5 public key that are stored as an immutable state, and balance/nonce.

Spend transaction size - 250 bytes

INTRINSIC_GAS = 1.4e-07 * 250 + 5.5e-10 * 3 = 3.5e-05 USD = 140 smidges
FIXED_GAS = 30 * 1.4e-07 * 16 + 1.4e-08 * 176 = 7e-05 USD = 278 smidges
TOTAL_GAS = 418 smidges

1 Like

Thanks @dmitry !

The benchmarks are extremely useful. The main thing that I think we need to change is that the gas cost is a relative measure. That is, it’s denominated in “gas”, not smesh, and the conversion between gas and smesh is done at execution time.

Computation cost

Given your benchmarks, it sounds reasonable to base the gas unit on a signature cost of 2^{-13}\approx 122\mu{s} seconds.

1 signature: 2^{8} gas
(this is nice and round and lets us define things that are slightly less expensive as well; if we don’t care about fine granularity we can set it to 1 gas instead)

Computing Storage Cost

I would convert storage into CPU cost using our initialization cost estimates, rather than the cheap disk cost. This gives a direct translation between storage and CPU, so we don’t have to involve assumptions about the market price of smesh. I think we can use time as a rough estimate here, so if our storage unit is 2^{38} bytes and takes ~2^{16} seconds on a mid-tier machine (using powers of 2 to make calculations nicer – this is about 18 hours), then the “cost” per byte is 2^{-22} seconds per byte per node per epoch.

We need two additional factors: a “replication” factor (how many nodes are storing the data) and a “horizon” (how many epochs will this data be stored; i.e., what counts as “forever”). I propose 2^{12}=4096 for the replication factor, and 128 epochs for the horizon (about 5 years), giving a total factor of 2^{19}.

Multiplying, we get a per byte “cost” of 2^{-3} seconds for storage. This means a byte of storage should cost as much as 2^{10} signatures:

1 byte of long-term storage: 2^{18} gas.

Active state cost

The relative cost of the active state is more tricky to price. It’s probably reasonable to have the “storage horizon” for the active state the same as for the long-term state, but I think the replication factor should probably be larger (since many non-smeshing nodes will be holding replicas of the state). For example, replication factor 2^{13} (twice the factor for long-term storage) might be reasonable.

Also, different active-state operations have different costs

  • Updating active state. In this case, the active-state size doesn’t increase but there’s an I/O operation involved. We should benchmark this on mid-tier hardware and use the time it takes to update a byte on a disk relative to the “canonical” signature time to set the gas price.

    Using the first internet benchmark I found for random write speeds of mid-tier SSDs, we have something like 32MB/s (=2^{25} bytes per second). This means that writing 1 byte to a random location would be 2^{-25} seconds, or 2^{-12} of a signature, but replicated over 2^{13} nodes it should cost 2 signatures.

    Updating 1 byte of active-state storage: 2^{9} gas

    I think that any writing operation should also pay this update cost.

  • Reading active state. This is similar to update, but for a read I/O operation. Using the same internet benchmark, reads are almost twice as fast as writes, so the cost should be half:

    Reading 1 byte of active-state storage: 2^{8} gas

  • Increasing active state size. This forces nodes to store additional data. Using the same calculation as for long-term storage, but with the larger replication factor:

    1 (additional) byte of active-state storage: 2^{19} gas.

  • Reducing active state size. Unlike long-term storage, it’s possible to reduce the active state size. In this case, I think it makes sense to get a “rebate” (IIRC, ethereum does this too). It’s not clear whether the rebate should be equivalent to the increase in state size. But IIRC, we don’t have any transactions that do this at genesis, so we don’t have to decide yet.

Example calculation using your benchmark values:

Single sig wallet

  • Spawn
    Computation: 1 sig - 2^{8} gas
    Long-term storage: 150 bytes = 150\cdot 2^{18} gas
    Increase in active-state size: 150 bytes = 150\cdot 2^{19} gas
    Update I/O cost: 150 bytes = 150 \cdot 2^9 gas
    Total: 118,041,856\approx 2^{27} gas
  • Spend
    Computation: 1 sig - 2^{8} gas
    Long-term storage: 120 bytes = 120\cdot 2^{18} gas
    Update I/O cost (balance+nonce): 16 bytes = 16\cdot 2^{9} gas
    Total: 31,465,728\approx 2^{25} gas
1 Like

thank you, thats very useful. need to ask a few clarifications:

That is, it’s denominated in “gas”, not smesh, and the conversion between gas and smesh is done at execution time.

i denominated gas in smidge because it is a minimal unit in the system, both gas and gas price are u64. for example if gas is 118,041,856 what would be a reasonable way to convert it?

Updating active state. In this case, the active-state size doesn’t increase but there’s an I/O operation involved.

so technically we don’t have updates, as we keep versioned storage. very old versions might be pruned from the state, but if they are pruned node may have to recompute from scratch in rare conditions.

Gas is in relative units because the exchange rate to smesh changes over time (since gas cost is fixed once at genesis, whereas the actual payment is in real-world currency, and the buying power of smesh will fluctuate)

IIRC, we didn’t completely finalize the mechanism for setting the exchange rate (in smidge-per-gas), but here is one possible mechanisms:
0. Every transaction includes its requested gas price (in smidge-per-gas) [This is already implemented, right?]

  1. Each proposal includes the minimal gas price at which the smesher is willing to accept transactions.
  2. The actual gas price for that layer is the median gas price of the proposals that are used to construct the UCB. We filter out every transaction whose gas price is below the median (we can do this for every block, not just in the optimistic filtering case, since it doesn’t need agreement on previous state). [Note that this isn’t super secure, since the number of proposals is small, and there may occasionally be a dishonest majority. However, in that case, the “worst” the attacker can do is set the price too high (in which case too few transactions get in) or too low (in which case some transactions get in “for free”), if this happens very rarely I don’t think it’s a big deal]
  3. Every transaction pays gas according to its stated gas price. (alternatively, we can have every transaction pay according to the median gas price, similar to a second-price auction)

So, for example, if a transaction costs 118,041,856 gas (according to the gas cost table), and the transaction’s gas price is 0.5 (smidge-per-gas) then the transaction would pay 59,020,928 smidge.

This sounds like an implementation detail. However, if we expect that every implementation will do this as well, then we should make the price of updates higher (or even equal to the price of increasing the state size).

Every transaction includes its requested gas price (in smidge-per-gas) [This is already implemented, right?]

yes, gas-price field is u64 integer. we do multiply gas by gas price to get the fee, but i don’t understand how we can express 0.5 smidge per gas.

I agree with @dmitry: gas price is (and should remain) an unsigned integer.

I don’t think we should account for operations smaller than a signature, or at least not much smaller. By lowering the gas cost by a factor of 256 we allow more granularity in the gas price.

i changed approach a bit, i looked up how ethereum estimates gas for transaction, using Appendix G. Fee Schedule Yellow paper , accounting for some changes in go-ethereum codebase.

name eth spacemesh description
txdata 512 2 charged for storing transaction data (per 32 bytes)
store 20000 78 charged for changing active state from zero to non-zero (per 32 bytes)
update 2900 11 charged for updating active state (per 32 bytes)
load 800 3 charged for loading active state (per 32 bytes)
edverify 3000 11 charged for ed25519 verification (per signature)
spawn 32000 125 charged for every spawn transaction
tx 21000 82 charged for every transaction

eth column uses reference values, spacemesh - scaled down by a factor of 256. i am not sure if we need to use scaled-down values, maybe we want to use factor lower than 256. if we are not using scaled down values, assuming market cap of 200bn and 2400000000000000000 smidges in circulation, single smidge cost is 8.3e-08 USD, a price for singlesig spend is ~0.003 USD.

pricing transactions

Every transaction pays base fee of 21000 gas, if it is a spawn transaction it also pays additionally 32000 gas. Beside base cost, intrinsic gas also includes cost for verification (edverify multiplied by number of signatures) and cost to store transaction (txdata multiplied by size is 32 words, rounded up).

Execution cost is based on stored/updated/loaded data. Couple of examples: singlesig self spawn stores 48 bytes (public key, amount, nonce). singlesig spend loads 48 bytes for principal, loads 8 bytes for destination account, and updates both principal and destination. multisig 3/5 spawn - stores 176 (5 public keys, amount , nonce).

name gas (eth) gas (spacemesh) usd (eth)
singlesig/selfspawn 98560 384 0.0082
singlesig/spend 34248 132 0.0028
singlesig/spawn 102260 398 0.0085
multisig/3/5/selfspawn 188656 734 0.0157
multisig/3/5/spend 45496 174 0.0038
multisig/3/10/selfspawn 291216 1134 0.0242
multisig/3/10/spend 49496 189 0.0041

usd(eth) column is using market cap “estimation” that i shared above. source code

how it compares with actual execution?

@iddo suggested to collect benchmarks how transaction execution speed compares to other transactions results. i shared them in linked pr, however i don’t see how they are useful for determining transactions cost. for instance selfspawn transactions are the fastest, because they load and update only one account. but we can’t make them the cheapest, as they add the most of the active state.

in my gas estimation i reflected cost of additional load/updates, but store is significantly more expensive which makes spawn transaction more expensive, as they should be.

problems

selfspawn vs spawn gas

in the implementation selfspawn and spawn is using the same gas. it is incorrest, if singlesig spawns multisig it should pay execution price of the multisig template. the solution to this is relatively simple, instead of looking up both intrinsic gas and execution gas from the same table, we will do two lookups one for principal template and another for template that is used in spawn.

fixed gas inconsistency

we are doing 3 different multisig (and vesting) templates (1/k, 2/k, 3/k). the assumption there was that parameter k will not change gas cost. this assumption was clearly wrong, creating (and loading) template with 3/10 (where k=10) and 3/5 should have different cost. as a stop gap solution we can always charge by the upper bound (k=10), but it doesn’t look nice.

1 Like

Echoing what @talm said above, gas cost is denominated in the arbitrary, relative unit of “gas” not in USD. The market will sort out the exchange rate between these two. It’s not meaningful to refer to the smidge- or fiat-denominated cost of a single tx without further context: e.g., “the minimum cost for a singlesig spend when demand for blockspace is low” or “the expected cost when demand rises.”

I disagree. The ratio in the eth gas table between the intrinsic cost of a single signature operation (ECRECOVER, 3000) and the minimum unit (1 or 2 gas, for storing one byte of calldata or “blob” data) is greater than 256. Why do we want to have a smaller ratio? 256 sounds okay to me; I’d be inclined to increase it to 1024 or 2048 to give us more granularity in pricing “cheap” operations.

Right. My inclination is to use a EIP1559-style fee market with a protocol-enforced basefee that moves up and down by a bounded multiplier each layer. The game theory here is quite nuanced so it feels much safer to use a tried-and-tested algorithm rather than trying to invent our own. Let’s discuss.

This also has some nuanced game theory and has led to the creation of a tokenized market for gas. I don’t think we want to encourage this (and IIRC the plan in Ethereum is to move away from this model). In this case I think we should keep things as simple as possible and just not include a refund for now; we can revisit this later as part of investigating solutions for pruning or stateless clients.

One thing that’s missing from the table is the difference between “warm” and “cold” state access: the initial cost of accessing an account or piece of storage, vs. the cost of reading/writing state that’s already been loaded. @dmitry is this relevant for us at genesis?

I’d go with a replication factor of 2^{16} = 65536 or 2^{17} which is much more in line with our actual, expected number of nodes. And I’d use 2^{10} = 1024 epochs for the horizon factor, for a total factor of 2^{26}. But this produces a very expensive result.

By your logic, multiplying produces a per-byte “cost” of 2^4 seconds for storage, so a byte of storage should cost as much as 2^{17} signatures: that’s 2^{25} gas per byte of long-term storage.

I stumbled onto another inconsistency. It makes sense for a spawn transaction to be a bit more expensive than self-spawn transaction, as it needs to load two accounts and update two accounts.

For example consider simple “assembly” for self-spawn singlesig using instructions that i provided above:

EDVERIFY
STORE 48 // store public key, nonce, amount

For spawn it will be more work:

LOAD 48 // load public key, nonce, amount
EDVERIFY
STORE 48 // same as above but for another account
UPDATE 16 // update nonce, amount

The problem is that this additional cost can’t be factored in using existing gas model. One workaround that i see is to make gas evaluation aware of self-spawn/spawn difference.

@selfdual-brain @noam have you thought about something like this?

We discussed the problem with @dmitry on a call. Conclusions:

  • It seems inevitable to make the FixedGas logic aware of the transaction type (Spawn/SelfSpawn/LocalMethodCall/ForeignMethodCall/TemplateDeploy)

  • The gas calculation model, which is currently based on a “fictional assembly commands”, might benefit from adding yet another command representing intrinsic cost of bringing an account into the scope of currently processed transaction. Such a command, called for example “ACCOUNT_LOAD”, would represent the cost of whatever computation and memory allocation is needed for bringing an account into scope (but not taking into account the effort of loading the elements of immutable and mutable state, which are to be independently represented with suitable amount of LOAD assembly command). This approach seems similar to what Ethereum is doing.

1 Like

Gas cost parameters

Operations

name gas description
txdata (per 8) 128 charged for storing transaction data
store (per 8) 5000 charged for changing active state from zero to non-zero
update (per 8) 725 charged for updating active state
load (per 8) 182 charged for loading active state
account access 2500 base cost for accessing account from storage
edverify (per sig) 3000 charged for ed25519 verification
spawn 30000 charged for every spawn transaction
tx 20000 charged for every transaction

Computing tx cost

Every transaction is priced using operations from the table above.

TOTAL_GAS = INTRINSIC_GAS + ACCESS_PRINCIPAL_GAS + EXECUTE_GAS

INTRINSIC_GAS = INTRINSIC_TX_GAS + TXDATA * TX_SIZE
INTRINSIC_TX_GAS = EDVERIFY * SIG_NUM + TX + SPAWN (?)
ACCESS_PRINCIPAL_GAS = ACCOUNT_ACCESS + LOAD * PRINCIPAL_STATE_SIZE

EXECUTE_GAS_SPAWN = STORE * PRINCIPAL_STATE_SIZE + UPDATE * ACCOUNT_HEADER_SIZE
EXECUTE_GAS_SPEND = ACCOUNT_ACCESS + LOAD * ACCOUNT_HEADER_SIZE + 2 * UPDATE * ACCOUNT_HEADER_SIZE

For self-spawn ACCESS_PRINCIPAL_GAS is not charged.

Examples

Price in usd is computed as if single gas unit costs 8.3e-08.

name gas price (usd)
singlesig/selfspawn 85560 0.0071
singlesig/spawn 93102 0.0077
singlesig/spend 33369 0.0028
multisig/1/3/selfspawn 126584 0.0105
multisig/1/3/spawn 133082 0.0110
multisig/1/3/spend 34825 0.0029
vesting/1/3/spawnvault 112806 0.0094
vesting/1/3/drain 37572 0.0031
multisig/3/5/selfspawn 175656 0.0146
multisig/3/5/spawn 183610 0.0152
multisig/3/5/spend 44329 0.0037
vesting/3/5/spawnvault 123766 0.0103
vesting/3/5/drain 47076 0.0039
1 Like

@Bheleu (getting all my badges, out of way)

Is there not some potential advantage for SVM design instead of gas fee going to a single miner, but instead to the effective “pool” of qualified SVM node operators for that VM node type that are online and available? The advantage for this is it allows flexibility in determining gas fees for different VM types that will be running on potential multi VM node instances of SVM. Think of SVM as the SpaceMesh PoeT process where issuance nodes are vetted. This allows miners and node operators to provide capabilities to the network in which the network is lacking, and creates an “new VM node release” excitement on how to adjust hardware / software to support the new node type. Processes that operate on the Network Mesh may even split those processes up among different node servers, unless there is a process that requires two VM node types that are grouped in terms of inter-functionality.

Trying to bring the thought process along if this has not already been discussed. Really like a network that doesn’t require going to outside pools, because the network is the pool.

-Miner4Nine :v::vulcan_salute:

1 Like