diff --git a/_tutorials-python/rcdemo.md b/_tutorials-python/rcdemo.md new file mode 100644 index 0000000000000000000000000000000000000000..bfc0c9a5bc39a3ac6bf41c497b9eb877a72b4957 --- /dev/null +++ b/_tutorials-python/rcdemo.md @@ -0,0 +1,135 @@ +--- +title: 'PY: Resource Credit System Developer Guide' +position: 36 +description: "The goal of this guide is to demystify how resources and RC's work. The intended audience is developers working on Hive user interfaces, +applications, and client libraries." +layout: full +canonical_url: rcdemo.html +--- +Full, runnable src of [Power Up Hive](https://gitlab.syncad.com/hive/devportal/-/tree/master/tutorials/python/36_rcdemo) can be downloaded as part of: [tutorials/python](https://gitlab.syncad.com/hive/devportal/-/tree/master/tutorials/python) (or download just this tutorial: [devportal-master-tutorials-python-36_rcdemo.zip](https://gitlab.syncad.com/hive/devportal/-/archive/master/devportal-master.zip?path=tutorials/python/36_rcdemo)). + +### Statelessness + +First of all, a word about statelessness. A great deal of effort has gone into carefully separating stateful from stateless computations. +The reason for this is so that UI's and client libraries can execute stateless algorithms locally. Local computation is always to be +preferred to RPC-API for performance, stability, scalability and security. + +Unfortunately, client library support for RC algorithms is lacking. This tutorial, and its accompanying script, are intended to +provide guidance to UI and client library maintainers about how to add support for the RC system. + +# The RC demo script + +To get up and running, I (`theoretical`) have transcribed some key algorithms from C++ to Python. For example, how many resources +are consumed by a vote transaction? The `rcdemo` script allows us to find out: + +``` +>>> from rcdemo import * +>>> count = count_resources(vote_tx, vote_tx_size) +>>> count["resource_state_bytes"] +499232 +>>> print(json.dumps(count)) +{"resource_count": {"resource_history_bytes": 133, "resource_new_accounts": 0, "resource_market_bytes": 0, "resource_state_bytes": 499232, "resource_execution_time": 0}} +``` + +The `count_resources()` function is *stateless*. That means all of the information needed to do the calculation is contained in the transaction itself. It doesn't +depend on what's happening on the blockchain, or what other users are doing. [1] [2] [3] + +[1] Although it is possible that the calculation will change in future versions of `hived`, for example to correct the [bug](https://github.com/steemit/steem/issues/2972) where execution time is always reported as zero. + +[2] For convenience, some of the constants used in the calculation are exposed by the `size_info` member of `rc_api.get_resource_params()`. Only a `hived` version upgrade can change any values returned by `rc_api.get_resource_params()`, so it is probably okay to query that API once, on startup or when first needed, and then cache the result forever. Or even embed the result of `rc_api.get_resource_params()` in the source code of your library or application. + +[3] `rcdemo.py` requires you to also input the transaction size into `count_resources()`. This is because `rcdemo.py` was created to be a standalone script, without a dependence on any particular client library. If you are integrating `rcdemo.py` into a client library, you might consider using your library's serializer to calculate the transaction size automatically, so the caller of `count_resources()` doesn't have to specify it. + +### Resources + +Let's go into details on the different kinds of resources which are limited by the RC system. + +- `resource_history_bytes` : Number of bytes consumed by the transaction. +- `resource_new_accounts` : Number of accounts created by the transaction. +- `resource_market_bytes` : Number of bytes consumed by the transaction if it contains market operations. +- `resource_state_bytes` : Number of bytes of chain state needed to support the transaction. +- `resource_execution_time` : An estimate of how long the transaction will take to execute. Zero for now due to [#2972](https://github.com/steemit/steem/issues/2972). + +The resources have different scales. The resources use fixed-point arithmetic where one "tick" of the resource value is a "fractional" value of the resource. Right now, the resource scales are scattered in different places. The `count_resources()` result has the following scale: + +- `resource_history_bytes` : One byte is equal to `1`. +- `resource_new_accounts` : One account is equal to `1`. +- `resource_market_bytes` : One byte is equal to `1`. +- `resource_state_bytes` : One byte which must be stored forever is equal to the `hived` compile-time constant `STATE_BYTES_SCALE`, which is `10000`. Bytes which must be stored for a bounded amount of time are worth less than `10000`, depending on how long they need to be stored. The specific constants used in various cases are specified in the `resource_params["size_info"]["resource_state_bytes"]` fields. +- `resource_execution_time` : One nanosecond of CPU time is equal to `1`. The values are based on benchmark measurements made on a machine similar to `hive.blog` servers. Some rounding was performed, and a few operations' timings were adjusted to account for additional processing of the virtual operations they cause. + +### Resource pool levels + +Each resource has a global *pool* which is the number of resources remaining. The pool code supports fractional resources, the denominator is represented by the `resource_unit` parameter. So for example, since `resource_params["resource_params"]["resource_market_bytes"]["resource_dynamics_params"]["resource_unit"]` is `10`, a pool level of `15,000,000,000` actually represents `1,500,000,000` bytes. + +### Resource credits + +The RC cost of each resource depends on the following information: + +- How many resources are in the corresponding resource pool +- The global RC regeneration rate, which may be calculated as `total_vesting_shares` / ([`HIVE_RC_REGEN_TIME`]({{ '/tutorials-recipes/understanding-configuration-values.html#HIVE_RC_REGEN_TIME' | relative_url }}) / [`HIVE_BLOCK_INTERVAL`]({{ '/tutorials-recipes/understanding-configuration-values.html#HIVE_BLOCK_INTERVAL' | relative_url }}))` +- The price curve parameters in the corresponding `price_curve_params` object + +For convenience, `rcdemo.py` contains an `RCModel` class with all of this information in its fields. + +``` +>>> print(json.dumps(model.get_transaction_rc_cost( vote_tx, vote_tx_size ))) +{"usage": {"resource_count": {"resource_history_bytes": 133, "resource_new_accounts": 0, "resource_market_bytes": 0, "resource_state_bytes": 499232, "resource_execution_time": 0}}, "cost": {"resource_history_bytes": 42136181, "resource_new_accounts": 0, "resource_market_bytes": 0, "resource_state_bytes": 238436287, "resource_execution_time": 0}} +>>> sum(model.get_transaction_rc_cost( vote_tx, vote_tx_size )["cost"].values()) +280572468 +``` + +The `model` object created in `rcdemo.py` is an instance of `RCModel` which uses hardcoded values for its pool levels and global RC regeneration rate. These values were taken from the live network and hardcoded in the `rcdemo.py` source code in late September 2018. So the RC cost calculation provided out-of-the-box by `rcdemo.py` are approximately correct as of late September 2018, but will become inaccurate as the "live" values drift away from the hardcoded values. When integrating the `rcdemo.py` code into an application, client library, or another situation where RPC access is feasible, you should understand how your code will query a `hived` RPC endpoint for current values. (Some libraries will probably choose to do this RPC automagically, other libraries may want to leave this plumbing to user code.) + +### Transaction limits + +Suppose an account has 15 Hive Power. How much can it vote? + +``` +>>> vote_cost = sum(model.get_transaction_rc_cost( vote_tx, vote_tx_size )["cost"].values()) +>>> vote_cost +280572468 +>>> vote_cost * total_vesting_fund_hive / total_vesting_shares +138.88697555075086 +``` + +This is the amount of Hive Power (in satoshis) that would be needed by an account to transact once per 5 days ([`HIVE_RC_REGEN_TIME`]({{ '/tutorials-recipes/understanding-configuration-values.html#HIVE_RC_REGEN_TIME' | relative_url }})). +Our 15 SP account has 15000 SP, so it would be able to do `15000 / 138`, or about `108`, such transactions per 5 days. + +You can regard the number `138` (or `0.138`) as the "cost" of a "standardized" vote transaction. It plays an analogous role to a +transaction fee in Bitcoin, but it is not exactly a fee. Because the word "fee" implies giving up a permanent token with a limited, +controlled emission rate. It is the amount of SP which will allow a user an additional vote transaction every 5 days (but it might +be slightly more or less, if your vote transactions use a slightly different amount of resources.) + +### Integrating the demo script + +The `rcdemo.py` script is a standalone Python script with no dependencies, no network access, and no transaction serializer. It is a port +of the algorithms, and a few example transactions for demo purposes. + +Eventually, client library maintainers should integrate `rcdemo.py` or equivalent functionality into each Hive client library. Such integration +depends on the idioms and conventions of each particular client library, for example: + +- A client library with a minimalist, "explicit is better than implicit" philosophy may simply rename `rcdemo`, +delete the example code, add a tiny bit of glue code, and give it to clients largely as-is. +- A client library with an automagic, "invisible RPC" philosophy might provide a transaction may make substantial modifications to `rcdemo` +or expose an interface like `get_rc_cost(tx)` which would conveniently return the RC cost of a transaction. Since the RC cost depends on +chain state, this `get_rc_cost()` function would call `hived` RPC's (or use cached values) to get additional inputs needed by stateless +algorithms, such as resource pools, `total_vesting_shares`, etc. +- A client library which has a general policy of hard-coding constants from the `hived` C++ source code might distribute +`rc_api.get_resource_parameters()` as part of its source code, as `rc_api.get_resource_parameters()` results can only change in a new +version of `hived`. (Perhaps this kind of thing is somehow automated in the library's build scripts.) +- A client library whose maintainers don't want to be obligated to make new releases when values in `hived` change as part of a new release, +might instead choose to call `rc_api.get_resource_parameters()`. Adding extra performance and security overhead in exchange for the +convenience of asking `hived` to report these values. +- A client library with its own `Transaction` class might choose to implement class methods instead of standalone functions. +- A client library in JavaScript, Ruby or Go might transcribe the algorithms in `rcdemo.py` into a different language. + +As you can see, integrating support for the RC system into a Hive client library involves a number of architectural and +technical decisions. + +### To Run the tutorial + +1. [review dev requirements](getting_started.html) +1. `git clone https://gitlab.syncad.com/hive/devportal.git` +1. `cd devportal/tutorials/python/36_rcdemo` +1. `python rcdemo.py` diff --git a/_tutorials-recipes/calculate_rc_recipe.md b/_tutorials-recipes/calculate_rc_recipe.md index 77a9d83cb7a7fe4f94eb0bc4a39d4ce15b16bbd3..ce766f091aec613145fff66cb23b0a4c01274db7 100644 --- a/_tutorials-recipes/calculate_rc_recipe.md +++ b/_tutorials-recipes/calculate_rc_recipe.md @@ -7,7 +7,7 @@ layout: full canonical_url: calculate_rc_recipe.html --- -Since HF20 a Resource Credit (RC) system has been implemented to manage the number of transactions (comments, votes, transfers, etc) you can execute on the blockchain at any given time. This recipe will look at how to calculate your current RC and also what the current RC cost is for a given transaction. This recipe is far more 'basics oriented' than most. For a more in-depth description of how RC's work consume [this excellent RC demo](https://github.com/steemit/rcdemo) created by Hive's Blockchain Team. +Since HF20 a Resource Credit (RC) system has been implemented to manage the number of transactions (comments, votes, transfers, etc) you can execute on the blockchain at any given time. This recipe will look at how to calculate your current RC and also what the current RC cost is for a given transaction. This recipe is far more 'basics oriented' than most. For a more in-depth description of how RC's work consume [this excellent RC demo]({{ '/tutorials-python/rcdemo.html' | relative_url }}) created by Hive's Blockchain Team. ## Intro @@ -86,4 +86,4 @@ Additional info can also be found in [this article by hive user @holger80](https ## Allocation of RC to blockchain resources -An in depth look at how RC's are assigned to each of the three resources (CPU megacycles/state memory/history size) can be found in Hive's wiki articles for [RC Bandwidth System](https://github.com/steemit/steem/wiki/RC-Bandwidth-System) and [Parameters](https://github.com/steemit/steem/wiki/RC-Bandwidth-Parameters) +An in depth look at how RC's are assigned to each of the three resources (CPU megacycles/state memory/history size) can be found in Hive's wiki articles for [RC Bandwidth System]({{ '/tutorials-recipes/rc-bandwidth-system' | relative_url }}) and [Parameters]({{ '/tutorials-recipes/rc-bandwidth-parameters' | relative_url }}). diff --git a/_tutorials-recipes/rc-bandwidth-parameters.md b/_tutorials-recipes/rc-bandwidth-parameters.md new file mode 100644 index 0000000000000000000000000000000000000000..a6652830fab299f3ea7be4d3842ee0d840abcdc7 --- /dev/null +++ b/_tutorials-recipes/rc-bandwidth-parameters.md @@ -0,0 +1,124 @@ +--- +title: RC Bandwidth Parameters +position: 1 +description: Analyze the dynamics of the resource budget pool. +exclude: true +layout: full +canonical_url: rc-bandwidth-parameters.html +--- + +### Parameters + +Each pool has its own values of the following constants: + +- `budget_time` : Time interval for budgeting. +- `budget` : How much the resource increases linearly over `budget_time`. +- `half_life` : Time until the resource would decay to 50% of its initial size with no load or budget. +- `drain_time` : Time until user RC would decrease to 0% of its capacity in the hypothetical world where all users have saved maximum RC, use all RC and RC regen on this resource, and the resource pool is empty (so the price is at its maximum value and there is budget but no drain). +- `inelasticity_threshold` : Percentage of the equilibrium value that serves as a boundary between elastic and inelastic prices. + +### Computed values + +Let us analyze the dynamics of the resource budget pool as outlined [here]({{ '/tutorials-recipes/rc-bandwidth-system' | relative_url }}) with an eye to establishing values of the parameters. + +### Setting p_0 + +Let `p(x)` be the RC cost curve, `p(x) = A / (B + x)`. + +Let's denote as `p_0` the value of `p(0)`, which is of course `p_0 = A / B`. In other words,`p_0` is the price when there are no resources in the pool. Let's set up a worst-case scenario: + +- Suppose there are no resources in the pool +- Suppose all users have maximum RC reserves +- Suppose all users spend all RC reserves and regen on this resource +- Suppose all users run out of resources at time `t_drain` + +Then we can solve for `p_0` in terms of `t_drain` and the budget: + +``` +global_rc_capacity + global_rc_regen * drain_time = p_0 * budget * drain_time +p_0 = (global_rc_capacity + global_rc_regen * drain_time) / (budget * drain_time) +``` + +Suppose `drain_time = 1 hour`. Then the interpretation of this equation is that, in an hour, users in the aggregate can only muster RC equal to their RC capacity plus an hour's worth of RC regen, call this much RC `rc_1h`. If we say this RC "should" be *just enough* to pay for `res_1h`, 1 hour's worth of a single resource (say, an hour's worth of history bytes). That implies a resource price of `rc_1h / res_1h`, which is the price we set the value of `p_0` to. + +If we substitute for `global_rc_capacity` and simplify, we get an alternative expression for `p_0` which may be useful for some analysis: + +``` +p_0 = (global_rc_capacity + global_rc_regen * drain_time) / (budget * drain_time) + = (global_rc_regen * rc_regen_time + global_rc_regen * drain_time) / (budget * drain_time) + = (global_rc_regen / budget) * (1 + rc_regen_time / drain_time) +``` + +The first multiplicative term, `global_rc_regen / budget`, is the *balanced budget price* which would cause spending to match outflow; let us say `p_bb = global_rc_regen / budget`. This is the equilibrium price if users have no reserves and spend all RC's on this resource. This is multiplied by a multiplier which represents the temporary level the price can rise to, which is ultimately unsustainable due to finite reserves. + +### Setting B + +What about the constant `B`? When `x` is much smaller than `B`: + +- The value of `p(x)` is approximately `A / B` +- Price `p(x)` is (almost) *inelastic* with respect to pool size `x` +- A small percentage change in `x` doesn't change `p(x)` much) + +When `x` is much larger than `B` the opposite is true: + +- The value of `p(x)` is approximately `A / x` +- Price `p(x)` is (almost) *elastic* with respect to pool size `x` +- A small percentage change in `x` causes an almost equal in magnitude percentage change in `p(x)`) + +So while `p(x)` is never fully elastic or inelastic, you may think of `B` as being near the "midpoint" of the "transition" from almost perfectly inelastic (for small `x`) to almost perfectly elastic (for large `x`). This makes sense, since a perfectly elastic price is desirable (when possible) but unstable when the pool goes to zero (a halving of the pool results in the doubling of the price, meaning the price "doubles infinite times" as the pool goes toward zero, "halving infinite times"). + +Where should this transition occur? The simple answer is a small fraction of the pool's equilibrium size at no load, say `1%`, or perhaps `1/128`. + +#### Inelasticity proof + +A more rigorous way to say that `p(x)` is (almost) inelastic is that, for any small value `∊`, there exists some `x` small enough such that, for any small value `δ`, we have `f(x) / f((1+δ)*x) < 1+δ*∊`. (This can be interpreted as saying the percentage change in `f(x)` in response to a percentage change in `x` dies out as `x →0âº`.) + +To prove this, set `x = B*∊`. Then: + +``` +f(x) / f((1+δ)*x) = (A / (B+x)) / (A / (B+(1+δ)*x)) + = (B+x+δ*x) / (B+x) + = 1 + δ*x / (B+x) + < 1 + δ*x / B + = 1 + δ*∊ +``` + +#### Elasticity proof + +TODO: State/prove similar condition for elasticity case. + +### Decay rate + +Let `Ï„` be the [time constant](https://en.wikipedia.org/wiki/Time_constant) for the decay rate. (The decay constant is [proportional to](https://en.wikipedia.org/wiki/Time_constant#Exponential_decay) the half-life.) The value of `Ï„` is essentially how much time it takes for the system to "forget" about past non-usage, and places a "soft limit" on the possibility of a very long burst of very high activity being allowed as a result of "saving up" months or years worth of inactivity. The value of `Ï„` is per-resource, and we've determined that resources should have short-term and long-term versions with different `Ï„` values. For short-term `Ï„` value, I recommend 1 hour. For the long-term `Ï„` value, I recommend 15 days. + +If `Δt` time passes, then the exponential decay step of updating the pool should be `pool -= Δt * pool / Ï„`. + +The no-load equilibrium pool size then occurs when `Δt * pool_eq / Ï„ = Δt * budget_per_sec`, or `pool_eq = Ï„ * budget_per_sec`. + +### Discount term + +Above, we set implicitly the largest (zero-stockpile) price `p_0` based on `drain_time`. It may also be useful to set the smallest possible price `p_eq` to something other than the value implied by the above equations. To do this, we can simply shift the curve downward by a factor of `D`, and scale the curve up so `p(0) = p_0`. + +To elaborate: + +First, define `p(x)` with discount term `D` such that + +``` +p(x) = (A / (B + x)) - D +``` + +Then set `B = inelasticity_threshold * pool_eq`. For `A` and `D` we have two equations in two unknowns: + +``` +p_0 = (A / B) - D +p_eq = (A / (B + pool_eq)) - D +``` + +Solve both equations for `A`, set the two expressions equal to each other, and solve for `D`: + +``` +A = B*(p_0 + D) = (p_eq+D)*(B+pool_eq) +-> B*p_0 + B*D = B*p_eq + B*D + p_eq*pool_eq + D*pool_eq +-> B*(p_0 - p_eq) - p_eq*pool_eq = D*pool_eq +-> (B / pool_eq)*(p_0 - p_eq) - p_eq = D +``` diff --git a/_tutorials-recipes/rc-bandwidth-system.md b/_tutorials-recipes/rc-bandwidth-system.md new file mode 100644 index 0000000000000000000000000000000000000000..85fe2b4c265d6c2371550576d54fc28d9eaef170 --- /dev/null +++ b/_tutorials-recipes/rc-bandwidth-system.md @@ -0,0 +1,85 @@ +--- +title: RC Bandwidth System +position: 1 +description: All about the RC bandwidth system, the complete rewrite of the bandwidth system. +exclude: true +layout: full +canonical_url: rc-bandwidth-system.html +--- +See also [RC Bandwidth Parameters]({{ '/tutorials-recipes/rc-bandwidth-parameters' | relative_url }}) + +### Introduction + +The *RC bandwidth system* is a complete rewrite of the bandwidth system. Its goals include: + +- Enable simple, effective UI feedback to users about bandwidth usage and remaining bandwidth +- Simplify the mental model of what buying additional SP gives users +- Reduce or eliminate unstable feedback in current bandwidth system + +### History + +HF20: Initial implementation. + +### Resource credits + +Each account has a manabar called "resource credits." Resource credits have the following characteristics: + +- RC's are attached to a particular account and cannot be transferred +- An account's maximum RC is proportional to its VESTS +- Transacting uses RC +- Transactions which would cause a negative RC balance are blocked +- RC regenerates over time + +### Resources + +How many RC's are required for a transaction? Statelessly compute, for each transaction, how many of each *resource* it takes. Resources include: + +- CPU (mega)cycles +- State memory +- History size + +Then each resource has an exchange rate. If CPU cycles cost 5 RC / megacycle, state memory costs 8 RC / byte, and history size costs 4 RC / byte, a transaction which takes 2 megacycles, creates 50 bytes of state, and has a 150 byte transaction size will cost `2*5 + 50*8 + 150*4 = 1010 RC`. + +### Resource budget pools + +A *resource budget pool* for each resource type will be established. The resource budget pool will have a per-block linear increase, a per-block percentage decrease, and a per-transaction decrease. + +For example: + +- Suppose the per-block resource budget is 2500 megacycles, 5000 state bytes, and 25,000 history bytes. +- Suppose the per-block percentage decrease is 0.02% +- Suppose the pool currently contains 12,000,000 megacycles, 20,000,000 state bytes, and 80,000,000 history bytes. +- Suppose the above transaction (consuming 2 megacycles, 50 state bytes, and 150 history bytes) is the only transaction which occurs. + +We can compute the new values as follows: + +``` +// when transaction is processed +bp.megacycles -= 2; +bp.state_bytes -= 50; +bp.history_bytes -= 150; + +// per block additive +bp.megacycles += 2500; +bp.state_bytes += 5000; +bp.history_bytes += 25000; + +// per block multiplicative +// of course this would be implemented as integer arithmetic +bp.megacycles *= 0.9998; +bp.state_bytes *= 0.9998; +bp.history_bytes *= 0.9998; +``` + +### Resource pricing + +The resource budget pool can be viewed as the blockchain's "stockpile" of each resource, which it "sells" for RC. The price of each resource is based on the current level of the stockpile. Exactly how the price +is determined isn't very important, as long as it is a decreasing, smooth curve. + +The specific cost curve is: + +``` +p(x) = A / (B + x) +``` + +where `A` and `B` are parameters which may be set to different values for different resources. diff --git a/tutorials/python/36_rcdemo/rcdemo.py b/tutorials/python/36_rcdemo/rcdemo.py new file mode 100644 index 0000000000000000000000000000000000000000..cc9dbbc6f970e2e617e053871db20f3d21b35667 --- /dev/null +++ b/tutorials/python/36_rcdemo/rcdemo.py @@ -0,0 +1,692 @@ +#!/usr/bin/env python3 + +import collections + +class CountOperationVisitor(object): + + def __init__(self, size_info, exec_info): + self.market_op_count = 0 + self.new_account_op_count = 0 + self.state_bytes_count = 0 + self.execution_time_count = 0 + self.size_info = size_info + self.exec_info = exec_info + + def get_authority_byte_count( self, auth ): + return (self.size_info.authority_base_size + + self.size_info.authority_account_member_size * len(auth["account_auths"]) + + self.size_info.authority_key_member_size * len(auth["key_auths"]) + ) + + def visit_account_create_operation( self, op ): + self.state_bytes_count += ( + self.size_info.account_object_base_size + + self.size_info.account_authority_object_base_size + + self.get_authority_byte_count( op["owner"] ) + + self.get_authority_byte_count( op["active"] ) + + self.get_authority_byte_count( op["posting"] ) + ) + self.execution_time_count += self.exec_info.account_create_operation_exec_time + + def visit_account_create_with_delegation_operation( self, op ): + self.state_bytes_count += ( + self.size_info.account_object_base_size + + self.size_info.account_authority_object_base_size + + self.get_authority_byte_count( op["owner"] ) + + self.get_authority_byte_count( op["active"] ) + + self.get_authority_byte_count( op["posting"] ) + + self.size_info.vesting_delegation_object_base_size + ) + self.execution_time_count += self.exec_info.account_create_with_delegation_operation_exec_time + + def visit_account_witness_vote_operation( self, op ): + self.state_bytes_count += self.size_info.witness_vote_object_base_size + self.execution_time_count += self.exec_info.account_witness_vote_operation_exec_time + + def visit_comment_operation( self, op ): + self.state_bytes_count += ( + self.size_info.comment_object_base_size + + self.size_info.comment_object_permlink_char_size * len(op["permlink"].encode("utf8")) + + self.size_info.comment_object_parent_permlink_char_size * len(op["parent_permlink"].encode("utf8")) + ) + self.execution_time_count += self.exec_info.comment_operation_exec_time + + def visit_comment_payout_beneficiaries( self, bens ): + self.state_bytes_count += self.size_info.comment_object_beneficiaries_member_size * len(bens["beneficiaries"]) + + def visit_comment_options_operation( self, op ): + for e in op["extensions"]: + getattr(self, "visit_"+e["type"])(e["value"]) + self.execution_time_count += self.exec_info.comment_options_operation_exec_time + + def visit_convert_operation( self, op ): + self.state_bytes_count += self.size_info.convert_request_object_base_size + self.execution_time_count += self.exec_info.convert_operation_exec_time + + def visit_create_claimed_account_operation( self, op ): + self.state_bytes_count += ( + self.size_info.account_object_base_size + + self.size_info.account_authority_object_base_size + + self.get_authority_byte_count( op["owner"] ) + + self.get_authority_byte_count( op["active"] ) + + self.get_authority_byte_count( op["posting"] ) + ) + self.execution_time_count += self.exec_info.create_claimed_account_operation_exec_time + + def visit_decline_voting_rights_operation( self, op ): + self.state_bytes_count += self.size_info.decline_voting_rights_request_object_base_size + self.execution_time_count += self.exec_info.decline_voting_rights_operation_exec_time + + def visit_delegate_vesting_shares_operation( self, op ): + self.state_bytes_count += max( + self.size_info.vesting_delegation_object_base_size, + self.size_info.vesting_delegation_expiration_object_base_size + ) + self.execution_time_count += self.exec_info.delegate_vesting_shares_operation_exec_time + + def visit_escrow_transfer_operation( self, op ): + self.state_bytes_count += self.size_info.escrow_object_base_size + self.execution_time_count += self.exec_info.escrow_transfer_operation_exec_time + + def visit_limit_order_create_operation( self, op ): + self.state_bytes_count += 0 if op["fill_or_kill"] else self.size_info.limit_order_object_base_size + self.execution_time_count += self.exec_info.limit_order_create_operation_exec_time + self.market_op_count += 1 + + def visit_limit_order_create2_operation( self, op ): + self.state_bytes_count += 0 if op["fill_or_kill"] else self.size_info.limit_order_object_base_size + self.execution_time_count += self.exec_info.limit_order_create2_operation_exec_time + self.market_op_count += 1 + + def visit_request_account_recovery_operation( self, op ): + self.state_bytes_count += self.size_info.account_recovery_request_object_base_size + self.execution_time_count += self.exec_info.request_account_recovery_operation_exec_time + + def visit_set_withdraw_vesting_route_operation( self, op ): + self.state_bytes_count += self.size_info.withdraw_vesting_route_object_base_size + self.execution_time_count += self.exec_info.set_withdraw_vesting_route_operation_exec_time + + def visit_vote_operation( self, op ): + self.state_bytes_count += self.size_info.comment_vote_object_base_size + self.execution_time_count += self.exec_info.vote_operation_exec_time + + def visit_witness_update_operation( self, op ): + self.state_bytes_count += ( + self.size_info.witness_object_base_size + + self.size_info.witness_object_url_char_size * len(op["url"].encode("utf8")) + ) + self.execution_time_count += self.exec_info.witness_update_operation_exec_time + + def visit_transfer_operation( self, op ): + self.execution_time_count += self.exec_info.transfer_operation_exec_time + self.market_op_count += 1 + + def visit_transfer_to_vesting_operation( self, op ): + self.execution_time_count += self.exec_info.transfer_to_vesting_operation_exec_time + self.market_op_count += 1 + + def visit_transfer_to_savings_operation( self, op ): + self.execution_time_count += self.exec_info.transfer_to_savings_operation_exec_time + + def visit_transfer_from_savings_operation( self, op ): + self.state_bytes_count += self.size_info.savings_withdraw_object_byte_size + self.execution_time_count += self.exec_info.transfer_from_savings_operation_exec_time + + def visit_claim_reward_balance_operation( self, op ): + self.execution_time_count += self.exec_info.claim_reward_balance_operation_exec_time + + def visit_withdraw_vesting_operation( self, op ): + self.execution_time_count += self.exec_info.withdraw_vesting_operation_exec_time + + def visit_account_update_operation( self, op ): + self.execution_time_count += self.exec_info.account_update_operation_exec_time + + def visit_account_witness_proxy_operation( self, op ): + self.execution_time_count += self.exec_info.account_witness_proxy_operation_exec_time + + def visit_cancel_transfer_from_savings_operation( self, op ): + self.execution_time_count += self.exec_info.cancel_transfer_from_savings_operation_exec_time + + def visit_change_recovery_account_operation( self, op ): + self.execution_time_count += self.exec_info.change_recovery_account_operation_exec_time + + def visit_claim_account_operation( self, op ): + self.execution_time_count += self.exec_info.claim_account_operation_exec_time + + if int(op["fee"]["amount"]) == 0: + self.new_account_op_count += 1 + + def visit_custom_operation( self, op ): + self.execution_time_count += self.exec_info.custom_operation_exec_time + + def visit_custom_json_operation( self, op ): + self.execution_time_count += self.exec_info.custom_json_operation_exec_time + + def visit_custom_binary_operation( self, op ): + self.execution_time_count += self.exec_info.custom_binary_operation_exec_time + + def visit_delete_comment_operation( self, op ): + self.execution_time_count += self.exec_info.delete_comment_operation_exec_time + + def visit_escrow_approve_operation( self, op ): + self.execution_time_count += self.exec_info.escrow_approve_operation_exec_time + + def visit_escrow_dispute_operation( self, op ): + self.execution_time_count += self.exec_info.escrow_dispute_operation_exec_time + + def visit_escrow_release_operation( self, op ): + self.execution_time_count += self.exec_info.escrow_release_operation_exec_time + + def visit_feed_publish_operation( self, op ): + self.execution_time_count += self.exec_info.feed_publish_operation_exec_time + + def visit_limit_order_cancel_operation( self, op ): + self.execution_time_count += self.exec_info.limit_order_cancel_operation_exec_time + + def visit_witness_set_properties_operation( self, op ): + self.execution_time_count += self.exec_info.witness_set_properties_operation_exec_time + + def visit_claim_reward_balance2_operation( self, op ): + self.execution_time_count += self.exec_info.claim_reward_balance2_operation_exec_time + + def visit_smt_setup_operation( self, op ): + self.execution_time_count += self.exec_info.smt_setup_operation_exec_time + + def visit_smt_cap_reveal_operation( self, op ): + self.execution_time_count += self.exec_info.smt_cap_reveal_operation_exec_time + + def visit_smt_refund_operation( self, op ): + self.execution_time_count += self.exec_info.smt_refund_operation_exec_time + + def visit_smt_setup_emissions_operation( self, op ): + self.execution_time_count += self.exec_info.smt_setup_emissions_operation_exec_time + + def visit_smt_set_setup_parameters_operation( self, op ): + self.execution_time_count += self.exec_info.smt_set_setup_parameters_operation_exec_time + + def visit_smt_set_runtime_parameters_operation( self, op ): + self.execution_time_count += self.exec_info.smt_set_runtime_parameters_operation_exec_time + + def visit_smt_create_operation( self, op ): + self.execution_time_count += self.exec_info.smt_create_operation_exec_time + + def visit_allowed_vote_assets( self, op ): + pass + + def visit_recover_account_operation( self, op ): pass + def visit_pow_operation( self, op ): pass + def visit_pow2_operation( self, op ): pass + def visit_report_over_production_operation( self, op ): pass + def visit_reset_account_operation( self, op ): pass + def visit_set_reset_account_operation( self, op ): pass + + # Virtual ops + def visit_fill_convert_request_operation( self, op ): pass + def visit_author_reward_operation( self, op ): pass + def visit_curation_reward_operation( self, op ): pass + def visit_comment_reward_operation( self, op ): pass + def visit_liquidity_reward_operation( self, op ): pass + def visit_interest_operation( self, op ): pass + def visit_fill_vesting_withdraw_operation( self, op ): pass + def visit_fill_order_operation( self, op ): pass + def visit_shutdown_witness_operation( self, op ): pass + def visit_fill_transfer_from_savings_operation( self, op ): pass + def visit_hardfork_operation( self, op ): pass + def visit_comment_payout_update_operation( self, op ): pass + def visit_return_vesting_delegation_operation( self, op ): pass + def visit_comment_benefactor_reward_operation( self, op ): pass + def visit_producer_reward_operation( self, op ): pass + def visit_clear_null_account_balance_operation( self, op ): pass + +class SizeInfo(object): + pass + +class ExecInfo(object): + pass + +class ResourceCounter(object): + def __init__(self, resource_params): + self.resource_params = resource_params + self.resource_name_to_index = {} + self._size_info = None + self._exec_info = None + + self.resource_names = self.resource_params["resource_names"] + self.STEEM_NUM_RESOURCE_TYPES = len(self.resource_names) + for i, resource_name in enumerate(self.resource_names): + self.resource_name_to_index[ resource_name ] = i + self._size_info = SizeInfo() + for k, v in self.resource_params["size_info"]["resource_state_bytes"].items(): + setattr(self._size_info, k, v) + self._exec_info = ExecInfo() + for k, v in self.resource_params["size_info"]["resource_execution_time"].items(): + setattr(self._exec_info, k, v) + return + + def __call__( self, tx=None, tx_size=-1 ): + if tx_size < 0: + ser = Serializer() + ser.signed_transaction(tx) + tx_size = len(ser.flush()) + result = collections.OrderedDict( + (("resource_count", collections.OrderedDict(( + ("resource_history_bytes", 0), + ("resource_new_accounts", 0), + ("resource_market_bytes", 0), + ("resource_state_bytes", 0), + ("resource_execution_time", 0), + ))),) + ) + + resource_count = result["resource_count"] + resource_count["resource_history_bytes"] += tx_size + + vtor = CountOperationVisitor(self._size_info, self._exec_info) + for op in tx["operations"]: + getattr(vtor, "visit_"+op["type"])(op["value"]) + resource_count["resource_new_accounts"] += vtor.new_account_op_count + + if vtor.market_op_count > 0: + resource_count["resource_market_bytes"] += tx_size + + resource_count["resource_state_bytes"] += ( + self._size_info.transaction_object_base_size + + self._size_info.transaction_object_byte_size * tx_size + + vtor.state_bytes_count ) + + # resource_count["resource_execution_time"] += vtor.execution_time_count + return result + +def compute_rc_cost_of_resource( curve_params=None, current_pool=0, resource_count=0, rc_regen=0 ): + if resource_count <= 0: + if resource_count < 0: + return -compute_rc_cost_of_resource( curve_params, current_pool, -resource_count, rc_regen ) + return 0 + num = rc_regen + num *= int(curve_params["coeff_a"]) + num >>= int(curve_params["shift"]) + num += 1 + num *= resource_count + + denom = int(curve_params["coeff_b"]) + denom += max(current_pool, 0) + + num_denom = num // denom + return num_denom+1 + +def rd_compute_pool_decay( + decay_params, + current_pool, + dt, + ): + if current_pool < 0: + return -rd_compute_pool_decay( decay_params, -current_pool, dt ) + decay_amount = int(decay_params["decay_per_time_unit"]) * dt + decay_amount *= current_pool + decay_amount >>= int(decay_params["decay_per_time_unit_denom_shift"]) + result = decay_amount + return min(result, current_pool) + +class RCModel(object): + def __init__(self, resource_params=None, resource_pool=None, rc_regen=0 ): + self.resource_params = resource_params + self.resource_pool = resource_pool + self.rc_regen = rc_regen + self.count_resources = ResourceCounter(resource_params) + self.resource_names = self.resource_params["resource_names"] + + def get_transaction_rc_cost(self, tx=None, tx_size=-1): + usage = self.count_resources( tx, tx_size ) + + total_cost = 0 + + cost = collections.OrderedDict() + + for resource_name in self.resource_params["resource_names"]: + params = self.resource_params["resource_params"][resource_name] + pool = int(self.resource_pool[resource_name]["pool"]) + + usage["resource_count"][resource_name] *= params["resource_dynamics_params"]["resource_unit"] + cost[resource_name] = compute_rc_cost_of_resource( params["price_curve_params"], pool, usage["resource_count"][resource_name], self.rc_regen) + total_cost += cost[resource_name] + # TODO: Port get_resource_user() + return collections.OrderedDict( (("usage", usage), ("cost", cost)) ) + + def apply_rc_pool_dynamics(self, count): + block_info = collections.OrderedDict(( + ("dt", collections.OrderedDict()), + ("decay", collections.OrderedDict()), + ("budget", collections.OrderedDict()), + ("usage", collections.OrderedDict()), + ("adjustment", collections.OrderedDict()), + ("pool", collections.OrderedDict()), + ("new_pool", collections.OrderedDict()), + )) + + for resource_name in self.resource_params["resource_names"]: + params = self.resource_params["resource_params"][resource_name]["resource_dynamics_params"] + pool = int(self.resource_pool[resource_name]["pool"]) + dt = 1 + + block_info["pool"][resource_name] = pool + block_info["dt"][resource_name] = dt + block_info["budget"][resource_name] = int(params["budget_per_time_unit"]) * dt + block_info["usage"][resource_name] = count[resource_name] * params["resource_unit"] + block_info["decay"][resource_name] = rd_compute_pool_decay( params["decay_params"], pool - block_info["usage"][resource_name], dt ) + + block_info["new_pool"][resource_name] = pool - block_info["decay"][resource_name] + block_info["budget"][resource_name] - block_info["usage"][resource_name] + return block_info + +# These are constants #define in the code +STEEM_RC_REGEN_TIME = 60*60*24*5 +STEEM_BLOCK_INTERVAL = 3 + +# This is the result of rc_api.get_resource_params() +resource_params = { + "resource_names": [ + "resource_history_bytes", + "resource_new_accounts", + "resource_market_bytes", + "resource_state_bytes", + "resource_execution_time" + ], + "resource_params": { + "resource_history_bytes": { + "resource_dynamics_params": { + "resource_unit": 1, + "budget_per_time_unit": 347222, + "pool_eq": "216404314004", + "max_pool_size": "432808628007", + "decay_params": { + "decay_per_time_unit": 3613026481, + "decay_per_time_unit_denom_shift": 51 + }, + "min_decay": 0 + }, + "price_curve_params": { + "coeff_a": "12981647055416481792", + "coeff_b": 1690658703, + "shift": 49 + } + }, + "resource_new_accounts": { + "resource_dynamics_params": { + "resource_unit": 10000, + "budget_per_time_unit": 797, + "pool_eq": 157691079, + "max_pool_size": 157691079, + "decay_params": { + "decay_per_time_unit": 347321, + "decay_per_time_unit_denom_shift": 36 + }, + "min_decay": 0 + }, + "price_curve_params": { + "coeff_a": "16484671763857882971", + "coeff_b": 1231961, + "shift": 51 + } + }, + "resource_market_bytes": { + "resource_dynamics_params": { + "resource_unit": 10, + "budget_per_time_unit": 578704, + "pool_eq": "16030041350", + "max_pool_size": "32060082699", + "decay_params": { + "decay_per_time_unit": 2540365427, + "decay_per_time_unit_denom_shift": 46 + }, + "min_decay": 0 + }, + "price_curve_params": { + "coeff_a": "9231393461629499392", + "coeff_b": 125234698, + "shift": 53 + } + }, + "resource_state_bytes": { + "resource_dynamics_params": { + "resource_unit": 1, + "budget_per_time_unit": 231481481, + "pool_eq": "144269542669147", + "max_pool_size": "288539085338293", + "decay_params": { + "decay_per_time_unit": 3613026481, + "decay_per_time_unit_denom_shift": 51 + }, + "min_decay": 0 + }, + "price_curve_params": { + "coeff_a": "12981647055416481792", + "coeff_b": "1127105802103", + "shift": 49 + } + }, + "resource_execution_time": { + "resource_dynamics_params": { + "resource_unit": 1, + "budget_per_time_unit": 82191781, + "pool_eq": "51225569123068", + "max_pool_size": "102451138246135", + "decay_params": { + "decay_per_time_unit": 3613026481, + "decay_per_time_unit_denom_shift": 51 + }, + "min_decay": 0 + }, + "price_curve_params": { + "coeff_a": "12981647055416481792", + "coeff_b": "400199758774", + "shift": 49 + } + } + }, + "size_info": { + "resource_state_bytes": { + "authority_base_size": 40000, + "authority_account_member_size": 180000, + "authority_key_member_size": 350000, + "account_object_base_size": 4800000, + "account_authority_object_base_size": 400000, + "account_recovery_request_object_base_size": 320000, + "comment_object_base_size": 2010000, + "comment_object_permlink_char_size": 10000, + "comment_object_parent_permlink_char_size": 20000, + "comment_object_beneficiaries_member_size": 180000, + "comment_vote_object_base_size": 470000, + "convert_request_object_base_size": 480000, + "decline_voting_rights_request_object_base_size": 280000, + "escrow_object_base_size": 1190000, + "limit_order_object_base_size": 147440, + "savings_withdraw_object_byte_size": 14656, + "transaction_object_base_size": 6090, + "transaction_object_byte_size": 174, + "vesting_delegation_object_base_size": 600000, + "vesting_delegation_expiration_object_base_size": 440000, + "withdraw_vesting_route_object_base_size": 430000, + "witness_object_base_size": 2660000, + "witness_object_url_char_size": 10000, + "witness_vote_object_base_size": 400000, + "STATE_BYTES_SCALE": 10000 + }, + "resource_execution_time": { + "account_create_operation_exec_time": 57700, + "account_create_with_delegation_operation_exec_time": 57700, + "account_update_operation_exec_time": 14000, + "account_witness_proxy_operation_exec_time": 117000, + "account_witness_vote_operation_exec_time": 23000, + "cancel_transfer_from_savings_operation_exec_time": 11500, + "change_recovery_account_operation_exec_time": 12000, + "claim_account_operation_exec_time": 10000, + "claim_reward_balance_operation_exec_time": 50300, + "comment_operation_exec_time": 114100, + "comment_options_operation_exec_time": 13200, + "convert_operation_exec_time": 15700, + "create_claimed_account_operation_exec_time": 57700, + "custom_operation_exec_time": 228000, + "custom_json_operation_exec_time": 228000, + "custom_binary_operation_exec_time": 228000, + "decline_voting_rights_operation_exec_time": 5300, + "delegate_vesting_shares_operation_exec_time": 19900, + "delete_comment_operation_exec_time": 51100, + "escrow_approve_operation_exec_time": 9900, + "escrow_dispute_operation_exec_time": 11500, + "escrow_release_operation_exec_time": 17200, + "escrow_transfer_operation_exec_time": 19100, + "feed_publish_operation_exec_time": 6200, + "limit_order_cancel_operation_exec_time": 9600, + "limit_order_create_operation_exec_time": 31700, + "limit_order_create2_operation_exec_time": 31700, + "request_account_recovery_operation_exec_time": 54400, + "set_withdraw_vesting_route_operation_exec_time": 17900, + "transfer_from_savings_operation_exec_time": 17500, + "transfer_operation_exec_time": 9600, + "transfer_to_savings_operation_exec_time": 6400, + "transfer_to_vesting_operation_exec_time": 44400, + "vote_operation_exec_time": 26500, + "withdraw_vesting_operation_exec_time": 10400, + "witness_set_properties_operation_exec_time": 9500, + "witness_update_operation_exec_time": 9500 + } + } +} + +# This is the result of get_resource_pool +resource_pool = { + "resource_history_bytes": { + "pool": "199290410749" + }, + "resource_new_accounts": { + "pool": 24573481 + }, + "resource_market_bytes": { + "pool": "15970580402" + }, + "resource_state_bytes": { + "pool": "132161364601521" + }, + "resource_execution_time": { + "pool": "47263115029450" + } +} + +# The value of total_vesting_shares is available from get_dynamic_global_properties() +total_vesting_shares = 397114288290855167 +total_vesting_fund_hive = 196576920519 + +example_transactions = { + "vote" : {"tx" : { + "ref_block_num": 12345, + "ref_block_prefix": 31415926, + "expiration": "2018-09-28T01:02:03", + "operations": [ + { + "type": "vote_operation", + "value": { + "voter": "alice1234567890", + "author": "bobob9876543210", + "permlink": "hello-world-its-bob", + "weight": 10000 + } + } + ], + "extensions": [], + "signatures": [ + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40" + ] + }, "tx_size" : 133}, + + "transfer" : {"tx" : { + "ref_block_num": 12345, + "ref_block_prefix": 31415926, + "expiration": "2018-09-28T01:02:03", + "operations": [ + { + "type": "transfer_operation", + "value": { + "from": "alice1234567890", + "to": "bobob9876543210", + "amount" : {"amount":"50000111","precision":3,"nai":"@@000000013"}, + "memo" : "#"+152*"x", + "permlink": "hello-world-its-bob", + "weight": 10000 + } + } + ], + "extensions": [], + "signatures": [ + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40" + ] + }, "tx_size" : 282}, + + "long_post" : {"tx" : { + "ref_block_num": 12345, + "ref_block_prefix": 31415926, + "expiration": "2018-09-28T01:02:03", + "operations":[{"type":"comment_operation","value":{ + "parent_author":"alice1234567890", + "parent_permlink":"itsover9000", + "author":"bobob9876543210", + "permlink":40*"p", + "title":40*"t", + "body":7000*"x", + "json_metadata":2000*"m" + }}, + {"type":"comment_options_operation", + "value":{"author":"bobob9876543210", + "permlink":40*"p", + "max_accepted_payout":{"amount":"1000000000","precision":3,"nai":"@@000000013"}, + "percent_hive_dollars":10000, + "allow_votes":True, + "allow_curation_rewards":True, + "extensions":[{"type":"comment_payout_beneficiaries","value":{"beneficiaries":[{"account":"coolui1997","weight":1000}]}}] + }}], + "extensions":[], + "signatures":["1f3de3651752238dcaa8ecfbc3a5c49bca50769b0f1af7d01def32578cf33184eb3258572754eefd036cb62adf797d6e3950fb943b5301fc6d6add64adcbe85f94"] + }, "tx_size" : 9303}, + + "short_post" : {"tx":{ + "ref_block_num": 12345, + "ref_block_prefix": 31415926, + "expiration": "2018-09-28T01:02:03", + "operations":[{"type":"comment_operation","value":{ + "parent_author":"alice1234567890", + "parent_permlink":10*"p", + "author":"bobob9876543210", + "permlink":40*"p", + "title":40*"t", + "body":500*"x", + "json_metadata":150*"m" + }}, + {"type":"comment_options_operation", + "value":{"author":"bobob9876543210", + "permlink":40*"p", + "max_accepted_payout":{"amount":"1000000000","precision":3,"nai":"@@000000013"}, + "percent_hive_dollars":10000, + "allow_votes":True, + "allow_curation_rewards":True, + "extensions":[{"type":"comment_payout_beneficiaries","value":{"beneficiaries":[{"account":"coolui1997","weight":1000}]}}] + }}], + "extensions":[], + "signatures":["1f3de3651752238dcaa8ecfbc3a5c49bca50769b0f1af7d01def32578cf33184eb3258572754eefd036cb62adf797d6e3950fb943b5301fc6d6add64adcbe85f94"]}, + "tx_size" : 952} + } + +vote_tx = example_transactions["vote"]["tx"] +vote_tx_size = example_transactions["vote"]["tx_size"] + +import json + +count_resources = ResourceCounter(resource_params) + +rc_regen = total_vesting_shares // (STEEM_RC_REGEN_TIME // STEEM_BLOCK_INTERVAL) +model = RCModel( resource_params=resource_params, resource_pool=resource_pool, rc_regen=rc_regen ) + +if __name__ == "__main__": + for example_name, etx in sorted(example_transactions.items()): + tx = etx["tx"] + tx_size = etx["tx_size"] + tx_cost = model.get_transaction_rc_cost(tx, tx_size) + total_cost = sum(tx_cost["cost"].values()) + print("{:20} {:6.3f}".format(example_name, total_cost * total_vesting_fund_hive / (1000.0 * total_vesting_shares)))