├── LICENSE.md ├── .gitignore ├── README.md └── rcdemo.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Steemit, Inc., and contributors. 2 | 3 | The following license applies to code contained within this repository that 4 | is created by Steemit, Inc. Other copy right holders have licensed dependencies such 5 | as Graphene, FC, and Boost under their own individual licenses. 6 | 7 | The MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # editor temp files 107 | *~* 108 | *#* 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resource Credit System developer guide 2 | 3 | The goal of this guide is to demystify how resources and RC's work. The intended audience is developers working on Steem user interfaces, 4 | applications, and client libraries. 5 | 6 | ### Statelessness 7 | 8 | First of all, a word about statelessness. A great deal of effort has gone into carefully separating stateful from stateless computations. 9 | The reason for this is so that UI's and client libraries can execute stateless algorithms locally. Local computation is always to be 10 | preferred to RPC-API for performance, stability, scalability and security. 11 | 12 | Unfortunately, client library support for RC algorithms is lacking. This tutorial, and its accompanying script, are intended to 13 | provide guidance to UI and client library maintainers about how to add support for the RC system. 14 | 15 | # The RC demo script 16 | 17 | To get up and running, I (`theoretical`) have transcribed some key algorithms from C++ to Python. For example, how many resources 18 | are consumed by a vote transaction? The `rcdemo` script allows us to find out: 19 | 20 | ``` 21 | >>> from rcdemo import * 22 | >>> count = count_resources(vote_tx, vote_tx_size) 23 | >>> count["resource_state_bytes"] 24 | 499232 25 | >>> print(json.dumps(count)) 26 | {"resource_count": {"resource_history_bytes": 133, "resource_new_accounts": 0, "resource_market_bytes": 0, "resource_state_bytes": 499232, "resource_execution_time": 0}} 27 | ``` 28 | 29 | 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 30 | depend on what's happening on the blockchain, or what other users are doing. [1] [2] [3] 31 | 32 | [1] Although it is possible that the calculation will change in future versions of `steemd`, for example to correct the [bug](https://github.com/steemit/steem/issues/2972) where execution time is always reported as zero. 33 | 34 | [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 `steemd` 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. 35 | 36 | [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. 37 | 38 | ### Resources 39 | 40 | Let's go into details on the different kinds of resources which are limited by the RC system. 41 | 42 | - `resource_history_bytes` : Number of bytes consumed by the transaction. 43 | - `resource_new_accounts` : Number of accounts created by the transaction. 44 | - `resource_market_bytes` : Number of bytes consumed by the transaction if it contains market operations. 45 | - `resource_state_bytes` : Number of bytes of chain state needed to support the transaction. 46 | - `resource_execution_time` : An estimate of how long the transaction will take to execute. Zero for now due to [Steem issue 2972](https://github.com/steemit/steem/issues/2972). 47 | 48 | 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: 49 | 50 | - `resource_history_bytes` : One byte is equal to `1`. 51 | - `resource_new_accounts` : One account is equal to `1`. 52 | - `resource_market_bytes` : One byte is equal to `1`. 53 | - `resource_state_bytes` : One byte which must be stored forever is equal to the `steemd` 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. 54 | - `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 `steemit.com` servers. Some rounding was performed, and a few operations' timings were adjusted to account for additional processing of the virtual operations they cause. 55 | 56 | ### Resource pool levels 57 | 58 | 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. 59 | 60 | ### Resource credits 61 | 62 | The RC cost of each resource depends on the following information: 63 | 64 | - How many resources are in the corresponding resource pool 65 | - The global RC regeneration rate, which may be calculated as `total_vesting_shares / (STEEM_RC_REGEN_TIME / STEEM_BLOCK_INTERVAL)` 66 | - The price curve parameters in the corresponding `price_curve_params` object 67 | 68 | For convenience, `rcdemo.py` contains an `RCModel` class with all of this information in its fields. 69 | 70 | ``` 71 | >>> print(json.dumps(model.get_transaction_rc_cost( vote_tx, vote_tx_size ))) 72 | {"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}} 73 | >>> sum(model.get_transaction_rc_cost( vote_tx, vote_tx_size )["cost"].values()) 74 | 280572468 75 | ``` 76 | 77 | 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 `steemd` 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.) 78 | 79 | ### Transaction limits 80 | 81 | Suppose an account has 15 Steem Power. How much can it vote? 82 | 83 | ``` 84 | >>> vote_cost = sum(model.get_transaction_rc_cost( vote_tx, vote_tx_size )["cost"].values()) 85 | >>> vote_cost 86 | 280572468 87 | >>> vote_cost * total_vesting_fund_steem / total_vesting_shares 88 | 138.88697555075086 89 | ``` 90 | 91 | This is the amount of Steem Power (in satoshis) that would be needed by an account to transact once per 5 days (`STEEM_RC_REGEN_TIME`). 92 | Our 15 SP account has 15000 SP, so it would be able to do `15000 / 138`, or about `108`, such transactions per 5 days. 93 | 94 | You can regard the number `138` (or `0.138`) as the "cost" of a "standardized" vote transaction. It plays an analogous role to a 95 | transaction fee in Bitcoin, but it is not exactly a fee. Because the word "fee" implies giving up a permanent token with a limited, 96 | controlled emission rate. It is the amount of SP which will allow a user an additional vote transaction every 5 days (but it might 97 | be slightly more or less, if your vote transactions use a slightly different amount of resources.) 98 | 99 | ### Integrating the demo script 100 | 101 | The `rcdemo.py` script is a standalone Python script with no dependencies, no network access, and no transaction serializer. It is a port 102 | of the algorithms, and a few example transactions for demo purposes. 103 | 104 | Eventually, client library maintainers should integrate `rcdemo.py` or equivalent functionality into each Steem client library. Such integration 105 | depends on the idioms and conventions of each particular client library, for example: 106 | 107 | - A client library with a minimalist, "explicit is better than implicit" philosophy may simply rename `rcdemo`, 108 | delete the example code, add a tiny bit of glue code, and give it to clients largely as-is. 109 | - A client library with an automagic, "invisible RPC" philosophy might provide a transaction may make substantial modifications to `rcdemo` 110 | 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 111 | chain state, this `get_rc_cost()` function would call `steemd` RPC's (or use cached values) to get additional inputs needed by stateless 112 | algorithms, such as resource pools, `total_vesting_shares`, etc. 113 | - A client library which has a general policy of hard-coding constants from the `steemd` C++ source code might distribute 114 | `rc_api.get_resource_parameters()` as part of its source code, as `rc_api.get_resource_parameters()` results can only change in a new 115 | version of `steemd`. (Perhaps this kind of thing is somehow automated in the library's build scripts.) 116 | - A client library whose maintainers don't want to be obligated to make new releases when values in `steemd` change as part of a new release, 117 | might instead choose to call `rc_api.get_resource_parameters()`. Adding extra performance and security overhead in exchange for the 118 | convenience of asking `steemd` to report these values. 119 | - A client library with its own `Transaction` class might choose to implement class methods instead of standalone functions. 120 | - A client library in JavaScript, Ruby or Go might transcribe the algorithms in `rcdemo.py` into a different language. 121 | 122 | As you can see, integrating support for the RC system into a Steem client library involves a number of architectural and 123 | technical decisions. 124 | -------------------------------------------------------------------------------- /rcdemo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import collections 4 | 5 | class CountOperationVisitor(object): 6 | 7 | def __init__(self, size_info, exec_info): 8 | self.market_op_count = 0 9 | self.new_account_op_count = 0 10 | self.state_bytes_count = 0 11 | self.execution_time_count = 0 12 | self.size_info = size_info 13 | self.exec_info = exec_info 14 | 15 | def get_authority_byte_count( self, auth ): 16 | return (self.size_info.authority_base_size 17 | + self.size_info.authority_account_member_size * len(auth["account_auths"]) 18 | + self.size_info.authority_key_member_size * len(auth["key_auths"]) 19 | ) 20 | 21 | def visit_account_create_operation( self, op ): 22 | self.state_bytes_count += ( 23 | self.size_info.account_object_base_size 24 | + self.size_info.account_authority_object_base_size 25 | + self.get_authority_byte_count( op["owner"] ) 26 | + self.get_authority_byte_count( op["active"] ) 27 | + self.get_authority_byte_count( op["posting"] ) 28 | ) 29 | self.execution_time_count += self.exec_info.account_create_operation_exec_time 30 | 31 | def visit_account_create_with_delegation_operation( self, op ): 32 | self.state_bytes_count += ( 33 | self.size_info.account_object_base_size 34 | + self.size_info.account_authority_object_base_size 35 | + self.get_authority_byte_count( op["owner"] ) 36 | + self.get_authority_byte_count( op["active"] ) 37 | + self.get_authority_byte_count( op["posting"] ) 38 | + self.size_info.vesting_delegation_object_base_size 39 | ) 40 | self.execution_time_count += self.exec_info.account_create_with_delegation_operation_exec_time 41 | 42 | def visit_account_witness_vote_operation( self, op ): 43 | self.state_bytes_count += self.size_info.witness_vote_object_base_size 44 | self.execution_time_count += self.exec_info.account_witness_vote_operation_exec_time 45 | 46 | def visit_comment_operation( self, op ): 47 | self.state_bytes_count += ( 48 | self.size_info.comment_object_base_size 49 | + self.size_info.comment_object_permlink_char_size * len(op["permlink"].encode("utf8")) 50 | + self.size_info.comment_object_parent_permlink_char_size * len(op["parent_permlink"].encode("utf8")) 51 | ) 52 | self.execution_time_count += self.exec_info.comment_operation_exec_time 53 | 54 | def visit_comment_payout_beneficiaries( self, bens ): 55 | self.state_bytes_count += self.size_info.comment_object_beneficiaries_member_size * len(bens["beneficiaries"]) 56 | 57 | def visit_comment_options_operation( self, op ): 58 | for e in op["extensions"]: 59 | getattr(self, "visit_"+e["type"])(e["value"]) 60 | self.execution_time_count += self.exec_info.comment_options_operation_exec_time 61 | 62 | def visit_convert_operation( self, op ): 63 | self.state_bytes_count += self.size_info.convert_request_object_base_size 64 | self.execution_time_count += self.exec_info.convert_operation_exec_time 65 | 66 | def visit_create_claimed_account_operation( self, op ): 67 | self.state_bytes_count += ( 68 | self.size_info.account_object_base_size 69 | + self.size_info.account_authority_object_base_size 70 | + self.get_authority_byte_count( op["owner"] ) 71 | + self.get_authority_byte_count( op["active"] ) 72 | + self.get_authority_byte_count( op["posting"] ) 73 | ) 74 | self.execution_time_count += self.exec_info.create_claimed_account_operation_exec_time 75 | 76 | def visit_decline_voting_rights_operation( self, op ): 77 | self.state_bytes_count += self.size_info.decline_voting_rights_request_object_base_size 78 | self.execution_time_count += self.exec_info.decline_voting_rights_operation_exec_time 79 | 80 | def visit_delegate_vesting_shares_operation( self, op ): 81 | self.state_bytes_count += max( 82 | self.size_info.vesting_delegation_object_base_size, 83 | self.size_info.vesting_delegation_expiration_object_base_size 84 | ) 85 | self.execution_time_count += self.exec_info.delegate_vesting_shares_operation_exec_time 86 | 87 | def visit_escrow_transfer_operation( self, op ): 88 | self.state_bytes_count += self.size_info.escrow_object_base_size 89 | self.execution_time_count += self.exec_info.escrow_transfer_operation_exec_time 90 | 91 | def visit_limit_order_create_operation( self, op ): 92 | self.state_bytes_count += 0 if op["fill_or_kill"] else self.size_info.limit_order_object_base_size 93 | self.execution_time_count += self.exec_info.limit_order_create_operation_exec_time 94 | self.market_op_count += 1 95 | 96 | def visit_limit_order_create2_operation( self, op ): 97 | self.state_bytes_count += 0 if op["fill_or_kill"] else self.size_info.limit_order_object_base_size 98 | self.execution_time_count += self.exec_info.limit_order_create2_operation_exec_time 99 | self.market_op_count += 1 100 | 101 | def visit_request_account_recovery_operation( self, op ): 102 | self.state_bytes_count += self.size_info.account_recovery_request_object_base_size 103 | self.execution_time_count += self.exec_info.request_account_recovery_operation_exec_time 104 | 105 | def visit_set_withdraw_vesting_route_operation( self, op ): 106 | self.state_bytes_count += self.size_info.withdraw_vesting_route_object_base_size 107 | self.execution_time_count += self.exec_info.set_withdraw_vesting_route_operation_exec_time 108 | 109 | def visit_vote_operation( self, op ): 110 | self.state_bytes_count += self.size_info.comment_vote_object_base_size 111 | self.execution_time_count += self.exec_info.vote_operation_exec_time 112 | 113 | def visit_witness_update_operation( self, op ): 114 | self.state_bytes_count += ( 115 | self.size_info.witness_object_base_size 116 | + self.size_info.witness_object_url_char_size * len(op["url"].encode("utf8")) 117 | ) 118 | self.execution_time_count += self.exec_info.witness_update_operation_exec_time 119 | 120 | def visit_transfer_operation( self, op ): 121 | self.execution_time_count += self.exec_info.transfer_operation_exec_time 122 | self.market_op_count += 1 123 | 124 | def visit_transfer_to_vesting_operation( self, op ): 125 | self.execution_time_count += self.exec_info.transfer_to_vesting_operation_exec_time 126 | self.market_op_count += 1 127 | 128 | def visit_transfer_to_savings_operation( self, op ): 129 | self.execution_time_count += self.exec_info.transfer_to_savings_operation_exec_time 130 | 131 | def visit_transfer_from_savings_operation( self, op ): 132 | self.state_bytes_count += self.size_info.savings_withdraw_object_byte_size 133 | self.execution_time_count += self.exec_info.transfer_from_savings_operation_exec_time 134 | 135 | def visit_claim_reward_balance_operation( self, op ): 136 | self.execution_time_count += self.exec_info.claim_reward_balance_operation_exec_time 137 | 138 | def visit_withdraw_vesting_operation( self, op ): 139 | self.execution_time_count += self.exec_info.withdraw_vesting_operation_exec_time 140 | 141 | def visit_account_update_operation( self, op ): 142 | self.execution_time_count += self.exec_info.account_update_operation_exec_time 143 | 144 | def visit_account_witness_proxy_operation( self, op ): 145 | self.execution_time_count += self.exec_info.account_witness_proxy_operation_exec_time 146 | 147 | def visit_cancel_transfer_from_savings_operation( self, op ): 148 | self.execution_time_count += self.exec_info.cancel_transfer_from_savings_operation_exec_time 149 | 150 | def visit_change_recovery_account_operation( self, op ): 151 | self.execution_time_count += self.exec_info.change_recovery_account_operation_exec_time 152 | 153 | def visit_claim_account_operation( self, op ): 154 | self.execution_time_count += self.exec_info.claim_account_operation_exec_time 155 | 156 | if int(op["fee"]["amount"]) == 0: 157 | self.new_account_op_count += 1 158 | 159 | def visit_custom_operation( self, op ): 160 | self.execution_time_count += self.exec_info.custom_operation_exec_time 161 | 162 | def visit_custom_json_operation( self, op ): 163 | self.execution_time_count += self.exec_info.custom_json_operation_exec_time 164 | 165 | def visit_custom_binary_operation( self, op ): 166 | self.execution_time_count += self.exec_info.custom_binary_operation_exec_time 167 | 168 | def visit_delete_comment_operation( self, op ): 169 | self.execution_time_count += self.exec_info.delete_comment_operation_exec_time 170 | 171 | def visit_escrow_approve_operation( self, op ): 172 | self.execution_time_count += self.exec_info.escrow_approve_operation_exec_time 173 | 174 | def visit_escrow_dispute_operation( self, op ): 175 | self.execution_time_count += self.exec_info.escrow_dispute_operation_exec_time 176 | 177 | def visit_escrow_release_operation( self, op ): 178 | self.execution_time_count += self.exec_info.escrow_release_operation_exec_time 179 | 180 | def visit_feed_publish_operation( self, op ): 181 | self.execution_time_count += self.exec_info.feed_publish_operation_exec_time 182 | 183 | def visit_limit_order_cancel_operation( self, op ): 184 | self.execution_time_count += self.exec_info.limit_order_cancel_operation_exec_time 185 | 186 | def visit_witness_set_properties_operation( self, op ): 187 | self.execution_time_count += self.exec_info.witness_set_properties_operation_exec_time 188 | 189 | def visit_claim_reward_balance2_operation( self, op ): 190 | self.execution_time_count += self.exec_info.claim_reward_balance2_operation_exec_time 191 | 192 | def visit_smt_setup_operation( self, op ): 193 | self.execution_time_count += self.exec_info.smt_setup_operation_exec_time 194 | 195 | def visit_smt_cap_reveal_operation( self, op ): 196 | self.execution_time_count += self.exec_info.smt_cap_reveal_operation_exec_time 197 | 198 | def visit_smt_refund_operation( self, op ): 199 | self.execution_time_count += self.exec_info.smt_refund_operation_exec_time 200 | 201 | def visit_smt_setup_emissions_operation( self, op ): 202 | self.execution_time_count += self.exec_info.smt_setup_emissions_operation_exec_time 203 | 204 | def visit_smt_set_setup_parameters_operation( self, op ): 205 | self.execution_time_count += self.exec_info.smt_set_setup_parameters_operation_exec_time 206 | 207 | def visit_smt_set_runtime_parameters_operation( self, op ): 208 | self.execution_time_count += self.exec_info.smt_set_runtime_parameters_operation_exec_time 209 | 210 | def visit_smt_create_operation( self, op ): 211 | self.execution_time_count += self.exec_info.smt_create_operation_exec_time 212 | 213 | def visit_allowed_vote_assets( self, op ): 214 | pass 215 | 216 | def visit_recover_account_operation( self, op ): pass 217 | def visit_pow_operation( self, op ): pass 218 | def visit_pow2_operation( self, op ): pass 219 | def visit_report_over_production_operation( self, op ): pass 220 | def visit_reset_account_operation( self, op ): pass 221 | def visit_set_reset_account_operation( self, op ): pass 222 | 223 | # Virtual ops 224 | def visit_fill_convert_request_operation( self, op ): pass 225 | def visit_author_reward_operation( self, op ): pass 226 | def visit_curation_reward_operation( self, op ): pass 227 | def visit_comment_reward_operation( self, op ): pass 228 | def visit_liquidity_reward_operation( self, op ): pass 229 | def visit_interest_operation( self, op ): pass 230 | def visit_fill_vesting_withdraw_operation( self, op ): pass 231 | def visit_fill_order_operation( self, op ): pass 232 | def visit_shutdown_witness_operation( self, op ): pass 233 | def visit_fill_transfer_from_savings_operation( self, op ): pass 234 | def visit_hardfork_operation( self, op ): pass 235 | def visit_comment_payout_update_operation( self, op ): pass 236 | def visit_return_vesting_delegation_operation( self, op ): pass 237 | def visit_comment_benefactor_reward_operation( self, op ): pass 238 | def visit_producer_reward_operation( self, op ): pass 239 | def visit_clear_null_account_balance_operation( self, op ): pass 240 | 241 | class SizeInfo(object): 242 | pass 243 | 244 | class ExecInfo(object): 245 | pass 246 | 247 | class ResourceCounter(object): 248 | def __init__(self, resource_params): 249 | self.resource_params = resource_params 250 | self.resource_name_to_index = {} 251 | self._size_info = None 252 | self._exec_info = None 253 | 254 | self.resource_names = self.resource_params["resource_names"] 255 | self.STEEM_NUM_RESOURCE_TYPES = len(self.resource_names) 256 | for i, resource_name in enumerate(self.resource_names): 257 | self.resource_name_to_index[ resource_name ] = i 258 | self._size_info = SizeInfo() 259 | for k, v in self.resource_params["size_info"]["resource_state_bytes"].items(): 260 | setattr(self._size_info, k, v) 261 | self._exec_info = ExecInfo() 262 | for k, v in self.resource_params["size_info"]["resource_execution_time"].items(): 263 | setattr(self._exec_info, k, v) 264 | return 265 | 266 | def __call__( self, tx=None, tx_size=-1 ): 267 | if tx_size < 0: 268 | ser = Serializer() 269 | ser.signed_transaction(tx) 270 | tx_size = len(ser.flush()) 271 | result = collections.OrderedDict( 272 | (("resource_count", collections.OrderedDict(( 273 | ("resource_history_bytes", 0), 274 | ("resource_new_accounts", 0), 275 | ("resource_market_bytes", 0), 276 | ("resource_state_bytes", 0), 277 | ("resource_execution_time", 0), 278 | ))),) 279 | ) 280 | 281 | resource_count = result["resource_count"] 282 | resource_count["resource_history_bytes"] += tx_size 283 | 284 | vtor = CountOperationVisitor(self._size_info, self._exec_info) 285 | for op in tx["operations"]: 286 | getattr(vtor, "visit_"+op["type"])(op["value"]) 287 | resource_count["resource_new_accounts"] += vtor.new_account_op_count 288 | 289 | if vtor.market_op_count > 0: 290 | resource_count["resource_market_bytes"] += tx_size 291 | 292 | resource_count["resource_state_bytes"] += ( 293 | self._size_info.transaction_object_base_size 294 | + self._size_info.transaction_object_byte_size * tx_size 295 | + vtor.state_bytes_count ) 296 | 297 | # resource_count["resource_execution_time"] += vtor.execution_time_count 298 | return result 299 | 300 | def compute_rc_cost_of_resource( curve_params=None, current_pool=0, resource_count=0, rc_regen=0 ): 301 | if resource_count <= 0: 302 | if resource_count < 0: 303 | return -compute_rc_cost_of_resource( curve_params, current_pool, -resource_count, rc_regen ) 304 | return 0 305 | num = rc_regen 306 | num *= int(curve_params["coeff_a"]) 307 | num >>= int(curve_params["shift"]) 308 | num += 1 309 | num *= resource_count 310 | 311 | denom = int(curve_params["coeff_b"]) 312 | denom += max(current_pool, 0) 313 | 314 | num_denom = num // denom 315 | return num_denom+1 316 | 317 | def rd_compute_pool_decay( 318 | decay_params, 319 | current_pool, 320 | dt, 321 | ): 322 | if current_pool < 0: 323 | return -rd_compute_pool_decay( decay_params, -current_pool, dt ) 324 | decay_amount = int(decay_params["decay_per_time_unit"]) * dt 325 | decay_amount *= current_pool 326 | decay_amount >>= int(decay_params["decay_per_time_unit_denom_shift"]) 327 | result = decay_amount 328 | return min(result, current_pool) 329 | 330 | class RCModel(object): 331 | def __init__(self, resource_params=None, resource_pool=None, rc_regen=0 ): 332 | self.resource_params = resource_params 333 | self.resource_pool = resource_pool 334 | self.rc_regen = rc_regen 335 | self.count_resources = ResourceCounter(resource_params) 336 | self.resource_names = self.resource_params["resource_names"] 337 | 338 | def get_transaction_rc_cost(self, tx=None, tx_size=-1): 339 | usage = self.count_resources( tx, tx_size ) 340 | 341 | total_cost = 0 342 | 343 | cost = collections.OrderedDict() 344 | 345 | for resource_name in self.resource_params["resource_names"]: 346 | params = self.resource_params["resource_params"][resource_name] 347 | pool = int(self.resource_pool[resource_name]["pool"]) 348 | 349 | usage["resource_count"][resource_name] *= params["resource_dynamics_params"]["resource_unit"] 350 | cost[resource_name] = compute_rc_cost_of_resource( params["price_curve_params"], pool, usage["resource_count"][resource_name], self.rc_regen) 351 | total_cost += cost[resource_name] 352 | # TODO: Port get_resource_user() 353 | return collections.OrderedDict( (("usage", usage), ("cost", cost)) ) 354 | 355 | def apply_rc_pool_dynamics(self, count): 356 | block_info = collections.OrderedDict(( 357 | ("dt", collections.OrderedDict()), 358 | ("decay", collections.OrderedDict()), 359 | ("budget", collections.OrderedDict()), 360 | ("usage", collections.OrderedDict()), 361 | ("adjustment", collections.OrderedDict()), 362 | ("pool", collections.OrderedDict()), 363 | ("new_pool", collections.OrderedDict()), 364 | )) 365 | 366 | for resource_name in self.resource_params["resource_names"]: 367 | params = self.resource_params["resource_params"][resource_name]["resource_dynamics_params"] 368 | pool = int(self.resource_pool[resource_name]["pool"]) 369 | dt = 1 370 | 371 | block_info["pool"][resource_name] = pool 372 | block_info["dt"][resource_name] = dt 373 | block_info["budget"][resource_name] = int(params["budget_per_time_unit"]) * dt 374 | block_info["usage"][resource_name] = count[resource_name] * params["resource_unit"] 375 | block_info["decay"][resource_name] = rd_compute_pool_decay( params["decay_params"], pool - block_info["usage"][resource_name], dt ) 376 | 377 | block_info["new_pool"][resource_name] = pool - block_info["decay"][resource_name] + block_info["budget"][resource_name] - block_info["usage"][resource_name] 378 | return block_info 379 | 380 | # These are constants #define in the code 381 | STEEM_RC_REGEN_TIME = 60*60*24*5 382 | STEEM_BLOCK_INTERVAL = 3 383 | 384 | # This is the result of rc_api.get_resource_params() 385 | resource_params = { 386 | "resource_names": [ 387 | "resource_history_bytes", 388 | "resource_new_accounts", 389 | "resource_market_bytes", 390 | "resource_state_bytes", 391 | "resource_execution_time" 392 | ], 393 | "resource_params": { 394 | "resource_history_bytes": { 395 | "resource_dynamics_params": { 396 | "resource_unit": 1, 397 | "budget_per_time_unit": 347222, 398 | "pool_eq": "216404314004", 399 | "max_pool_size": "432808628007", 400 | "decay_params": { 401 | "decay_per_time_unit": 3613026481, 402 | "decay_per_time_unit_denom_shift": 51 403 | }, 404 | "min_decay": 0 405 | }, 406 | "price_curve_params": { 407 | "coeff_a": "12981647055416481792", 408 | "coeff_b": 1690658703, 409 | "shift": 49 410 | } 411 | }, 412 | "resource_new_accounts": { 413 | "resource_dynamics_params": { 414 | "resource_unit": 10000, 415 | "budget_per_time_unit": 797, 416 | "pool_eq": 157691079, 417 | "max_pool_size": 157691079, 418 | "decay_params": { 419 | "decay_per_time_unit": 347321, 420 | "decay_per_time_unit_denom_shift": 36 421 | }, 422 | "min_decay": 0 423 | }, 424 | "price_curve_params": { 425 | "coeff_a": "16484671763857882971", 426 | "coeff_b": 1231961, 427 | "shift": 51 428 | } 429 | }, 430 | "resource_market_bytes": { 431 | "resource_dynamics_params": { 432 | "resource_unit": 10, 433 | "budget_per_time_unit": 578704, 434 | "pool_eq": "16030041350", 435 | "max_pool_size": "32060082699", 436 | "decay_params": { 437 | "decay_per_time_unit": 2540365427, 438 | "decay_per_time_unit_denom_shift": 46 439 | }, 440 | "min_decay": 0 441 | }, 442 | "price_curve_params": { 443 | "coeff_a": "9231393461629499392", 444 | "coeff_b": 125234698, 445 | "shift": 53 446 | } 447 | }, 448 | "resource_state_bytes": { 449 | "resource_dynamics_params": { 450 | "resource_unit": 1, 451 | "budget_per_time_unit": 231481481, 452 | "pool_eq": "144269542669147", 453 | "max_pool_size": "288539085338293", 454 | "decay_params": { 455 | "decay_per_time_unit": 3613026481, 456 | "decay_per_time_unit_denom_shift": 51 457 | }, 458 | "min_decay": 0 459 | }, 460 | "price_curve_params": { 461 | "coeff_a": "12981647055416481792", 462 | "coeff_b": "1127105802103", 463 | "shift": 49 464 | } 465 | }, 466 | "resource_execution_time": { 467 | "resource_dynamics_params": { 468 | "resource_unit": 1, 469 | "budget_per_time_unit": 82191781, 470 | "pool_eq": "51225569123068", 471 | "max_pool_size": "102451138246135", 472 | "decay_params": { 473 | "decay_per_time_unit": 3613026481, 474 | "decay_per_time_unit_denom_shift": 51 475 | }, 476 | "min_decay": 0 477 | }, 478 | "price_curve_params": { 479 | "coeff_a": "12981647055416481792", 480 | "coeff_b": "400199758774", 481 | "shift": 49 482 | } 483 | } 484 | }, 485 | "size_info": { 486 | "resource_state_bytes": { 487 | "authority_base_size": 40000, 488 | "authority_account_member_size": 180000, 489 | "authority_key_member_size": 350000, 490 | "account_object_base_size": 4800000, 491 | "account_authority_object_base_size": 400000, 492 | "account_recovery_request_object_base_size": 320000, 493 | "comment_object_base_size": 2010000, 494 | "comment_object_permlink_char_size": 10000, 495 | "comment_object_parent_permlink_char_size": 20000, 496 | "comment_object_beneficiaries_member_size": 180000, 497 | "comment_vote_object_base_size": 470000, 498 | "convert_request_object_base_size": 480000, 499 | "decline_voting_rights_request_object_base_size": 280000, 500 | "escrow_object_base_size": 1190000, 501 | "limit_order_object_base_size": 147440, 502 | "savings_withdraw_object_byte_size": 14656, 503 | "transaction_object_base_size": 6090, 504 | "transaction_object_byte_size": 174, 505 | "vesting_delegation_object_base_size": 600000, 506 | "vesting_delegation_expiration_object_base_size": 440000, 507 | "withdraw_vesting_route_object_base_size": 430000, 508 | "witness_object_base_size": 2660000, 509 | "witness_object_url_char_size": 10000, 510 | "witness_vote_object_base_size": 400000, 511 | "STATE_BYTES_SCALE": 10000 512 | }, 513 | "resource_execution_time": { 514 | "account_create_operation_exec_time": 57700, 515 | "account_create_with_delegation_operation_exec_time": 57700, 516 | "account_update_operation_exec_time": 14000, 517 | "account_witness_proxy_operation_exec_time": 117000, 518 | "account_witness_vote_operation_exec_time": 23000, 519 | "cancel_transfer_from_savings_operation_exec_time": 11500, 520 | "change_recovery_account_operation_exec_time": 12000, 521 | "claim_account_operation_exec_time": 10000, 522 | "claim_reward_balance_operation_exec_time": 50300, 523 | "comment_operation_exec_time": 114100, 524 | "comment_options_operation_exec_time": 13200, 525 | "convert_operation_exec_time": 15700, 526 | "create_claimed_account_operation_exec_time": 57700, 527 | "custom_operation_exec_time": 228000, 528 | "custom_json_operation_exec_time": 228000, 529 | "custom_binary_operation_exec_time": 228000, 530 | "decline_voting_rights_operation_exec_time": 5300, 531 | "delegate_vesting_shares_operation_exec_time": 19900, 532 | "delete_comment_operation_exec_time": 51100, 533 | "escrow_approve_operation_exec_time": 9900, 534 | "escrow_dispute_operation_exec_time": 11500, 535 | "escrow_release_operation_exec_time": 17200, 536 | "escrow_transfer_operation_exec_time": 19100, 537 | "feed_publish_operation_exec_time": 6200, 538 | "limit_order_cancel_operation_exec_time": 9600, 539 | "limit_order_create_operation_exec_time": 31700, 540 | "limit_order_create2_operation_exec_time": 31700, 541 | "request_account_recovery_operation_exec_time": 54400, 542 | "set_withdraw_vesting_route_operation_exec_time": 17900, 543 | "transfer_from_savings_operation_exec_time": 17500, 544 | "transfer_operation_exec_time": 9600, 545 | "transfer_to_savings_operation_exec_time": 6400, 546 | "transfer_to_vesting_operation_exec_time": 44400, 547 | "vote_operation_exec_time": 26500, 548 | "withdraw_vesting_operation_exec_time": 10400, 549 | "witness_set_properties_operation_exec_time": 9500, 550 | "witness_update_operation_exec_time": 9500 551 | } 552 | } 553 | } 554 | 555 | # This is the result of get_resource_pool 556 | resource_pool = { 557 | "resource_history_bytes": { 558 | "pool": "199290410749" 559 | }, 560 | "resource_new_accounts": { 561 | "pool": 24573481 562 | }, 563 | "resource_market_bytes": { 564 | "pool": "15970580402" 565 | }, 566 | "resource_state_bytes": { 567 | "pool": "132161364601521" 568 | }, 569 | "resource_execution_time": { 570 | "pool": "47263115029450" 571 | } 572 | } 573 | 574 | # The value of total_vesting_shares is available from get_dynamic_global_properties() 575 | total_vesting_shares = 397114288290855167 576 | total_vesting_fund_steem = 196576920519 577 | 578 | example_transactions = { 579 | "vote" : {"tx" : { 580 | "ref_block_num": 12345, 581 | "ref_block_prefix": 31415926, 582 | "expiration": "2018-09-28T01:02:03", 583 | "operations": [ 584 | { 585 | "type": "vote_operation", 586 | "value": { 587 | "voter": "alice1234567890", 588 | "author": "bobob9876543210", 589 | "permlink": "hello-world-its-bob", 590 | "weight": 10000 591 | } 592 | } 593 | ], 594 | "extensions": [], 595 | "signatures": [ 596 | "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40" 597 | ] 598 | }, "tx_size" : 133}, 599 | 600 | "transfer" : {"tx" : { 601 | "ref_block_num": 12345, 602 | "ref_block_prefix": 31415926, 603 | "expiration": "2018-09-28T01:02:03", 604 | "operations": [ 605 | { 606 | "type": "transfer_operation", 607 | "value": { 608 | "from": "alice1234567890", 609 | "to": "bobob9876543210", 610 | "amount" : {"amount":"50000111","precision":3,"nai":"@@000000013"}, 611 | "memo" : "#"+152*"x", 612 | "permlink": "hello-world-its-bob", 613 | "weight": 10000 614 | } 615 | } 616 | ], 617 | "extensions": [], 618 | "signatures": [ 619 | "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40" 620 | ] 621 | }, "tx_size" : 282}, 622 | 623 | "long_post" : {"tx" : { 624 | "ref_block_num": 12345, 625 | "ref_block_prefix": 31415926, 626 | "expiration": "2018-09-28T01:02:03", 627 | "operations":[{"type":"comment_operation","value":{ 628 | "parent_author":"alice1234567890", 629 | "parent_permlink":"itsover9000", 630 | "author":"bobob9876543210", 631 | "permlink":40*"p", 632 | "title":40*"t", 633 | "body":7000*"x", 634 | "json_metadata":2000*"m" 635 | }}, 636 | {"type":"comment_options_operation", 637 | "value":{"author":"bobob9876543210", 638 | "permlink":40*"p", 639 | "max_accepted_payout":{"amount":"1000000000","precision":3,"nai":"@@000000013"}, 640 | "percent_steem_dollars":10000, 641 | "allow_votes":True, 642 | "allow_curation_rewards":True, 643 | "extensions":[{"type":"comment_payout_beneficiaries","value":{"beneficiaries":[{"account":"coolui1997","weight":1000}]}}] 644 | }}], 645 | "extensions":[], 646 | "signatures":["1f3de3651752238dcaa8ecfbc3a5c49bca50769b0f1af7d01def32578cf33184eb3258572754eefd036cb62adf797d6e3950fb943b5301fc6d6add64adcbe85f94"] 647 | }, "tx_size" : 9303}, 648 | 649 | "short_post" : {"tx":{ 650 | "ref_block_num": 12345, 651 | "ref_block_prefix": 31415926, 652 | "expiration": "2018-09-28T01:02:03", 653 | "operations":[{"type":"comment_operation","value":{ 654 | "parent_author":"alice1234567890", 655 | "parent_permlink":10*"p", 656 | "author":"bobob9876543210", 657 | "permlink":40*"p", 658 | "title":40*"t", 659 | "body":500*"x", 660 | "json_metadata":150*"m" 661 | }}, 662 | {"type":"comment_options_operation", 663 | "value":{"author":"bobob9876543210", 664 | "permlink":40*"p", 665 | "max_accepted_payout":{"amount":"1000000000","precision":3,"nai":"@@000000013"}, 666 | "percent_steem_dollars":10000, 667 | "allow_votes":True, 668 | "allow_curation_rewards":True, 669 | "extensions":[{"type":"comment_payout_beneficiaries","value":{"beneficiaries":[{"account":"coolui1997","weight":1000}]}}] 670 | }}], 671 | "extensions":[], 672 | "signatures":["1f3de3651752238dcaa8ecfbc3a5c49bca50769b0f1af7d01def32578cf33184eb3258572754eefd036cb62adf797d6e3950fb943b5301fc6d6add64adcbe85f94"]}, 673 | "tx_size" : 952} 674 | } 675 | 676 | vote_tx = example_transactions["vote"]["tx"] 677 | vote_tx_size = example_transactions["vote"]["tx_size"] 678 | 679 | import json 680 | 681 | count_resources = ResourceCounter(resource_params) 682 | 683 | rc_regen = total_vesting_shares // (STEEM_RC_REGEN_TIME // STEEM_BLOCK_INTERVAL) 684 | model = RCModel( resource_params=resource_params, resource_pool=resource_pool, rc_regen=rc_regen ) 685 | 686 | if __name__ == "__main__": 687 | for example_name, etx in sorted(example_transactions.items()): 688 | tx = etx["tx"] 689 | tx_size = etx["tx_size"] 690 | tx_cost = model.get_transaction_rc_cost(tx, tx_size) 691 | total_cost = sum(tx_cost["cost"].values()) 692 | print("{:20} {:6.3f}".format(example_name, total_cost * total_vesting_fund_steem / (1000.0 * total_vesting_shares))) 693 | --------------------------------------------------------------------------------