EOSCommunity.org Forums

RAM consumption of eosio.token

Calling eosio.token::open on an account that does not yet have a balance results in the ram_payer consuming 240 bytes. And when the account that had its balance opened transacts for the first time the previous ram_payer only releases 128 bytes. What are the remaining 112 bytes used for and why aren’t the ownership of those also transferred to the account holder on the first transaction (which seems to be the intent according to the ricardian contract).

References: https://github.com/EOSIO/eosio.contracts/blob/master/contracts/eosio.token/src/eosio.token.cpp#L77-L147
https://github.com/EOSIO/eosio.contracts/blob/master/contracts/eosio.token/ricardian/eosio.token.contracts.md.in#L80-L95

2 Likes

Example in the wild:

Transaction that establishes the account balance
https://jungle3.bloks.io/transaction/358b659ea8ea4369075043f4a2545dd9cefba865bf2f7bd3f6bfed15b2a05414

Transaction that transfers ownership of the balance
https://jungle3.bloks.io/transaction/2a6b08eb84fd6ddb5ad35c8a81be1158c0e38e13e8ebcdbad3131b8a371e13d1

1 Like

RAM Billing for DB API within EOSIO Protocol

The following breakdown of RAM usage applies for the original database host function API that the eosio::multi_index wrapper leverages. It does not apply for the new key-value database API introduced in EOSIO 2.1.

The original database API maps a 64-bit integer key to a variable-sized binary blob value (see here for relevant host functions). These APIs take a code, scope, and table name. The 64-bit integer key must be unique within the context of a particular code, scope, table triplet. Each code, scope, table triplet is internally represented by structure in state which consumed space. Therefore, there is some amount of RAM charged for this “table metadata” structure. Specifically, 112 bytes is billed when creating an instance of this “table metadata” structure; this is the result of taking the 32 bytes overhead per internal index multiplying it by 2 for the two indices on that structure, adding 44 bytes to account for the serialized size of the structure’s fields, and then finally rounding up to the nearest 16 bytes. In addition, each table row takes up space within the state too and so RAM is billed for the table row as well. In that case, the number of bytes billed is 112 plus the serialized size in bytes of the value blob.

Note that the “table metadata” structure and the individual table rows can have distinct payers. When a new table row is added for a table that doesn’t have a “table metadata” structure (e.g. because no table rows with that specific triplet of code, scope, and table existed within the state prior to adding the new table row), then the payer of the “table metadata” structure is the same as the payer of the table row. However, the “table metadata” structure will continue to be billed to the payer that created it as long as there exists at least one table row which uses the corresponding triplet of names. This means that if Alice creates (and pays for) a new table with some initial row, then Bob creates (and pays for) a table row in that same table Alice created, and then Alice erases the table row he created, Alice will get refunding the bytes for the table row but will not (yet) be refunded the 112 bytes for the “table metadata” structure. Those 112 bytes will only be refunded to Alice when the last table row using that triplet of names is removed, e.g. when Bob removes his created table row in the prior example.

The original database API also has many different secondary indices to support the primary index mapping the 64-bit integer key to the variable-sized binary blob value. These secondary indices all map the secondary key (which varies across the five different secondary index types and is not required to be unique within the context of each code, scope, table triplet) to the primary key which is the same 64-bit integer used as the key in the primary index that mapped to the variable-sized binary blob. There are three secondary indices in which the keys are 64-bit, 128-bit, and 256-bit integers. Then there are two other secondary indices in which the keys are IEEE 754 floating-point numbers: one in which the key is a double-precision floating-point number, and another in which the key is a quadruple-precision floating-point number. All of these secondary indices can share the same “table metadata” structure with each other and with the primary index assuming they all use the same triple of code, scope, and table names.

Since there are no variable-length fields for the structures dealing with these secondary indices, one can determine the fixed RAM cost added per table row for each of these secondary indices. Both the 64-bit integer key secondary index and the double-precision floating-point key secondary index cause an additional 128 bytes of RAM to be billed per table row. Both the 128-bit integer key secondary index and the quadruple-precision floating-point key secondary index cause an additional 144 bytes of RAM to be billed per table row. And finally, the 256-bit integer key secondary index causes an additional 160 bytes of RAM to be billed per table row.

How RAM Billing Maps to Multi Index Wrapper in CDT

The eosio::multi_index wrapper builds on top of the host functions of the original database API referenced above. Each eosio::multi_index table most at a minimum use the primary index that maps the unique 64-bit integer key to the variable-sized binary blob value. This binary blob is the serialization of the struct used as part of the eosio::multi_index table. The eosio::multi_index wrapper also permits the developer to specify up to 16 secondary indices to use with that table.

The reason for the limit of 16 secondary indices is due to how the table name used in the host function API is abused to not only include the table name specified in the eosio::multi_index template instantiation but also to include a 4-bit ordinal representing which secondary index of this logical table it is referring to. So the eosio::multi_index restricts the table name to an eosio::name of up to 12 characters (which can be mapped by a 60-bit integer) so that it reserves the last 4-bit for that secondary index ordinal. Because it starts the count of the ordinal from 0, this means that the first secondary index will in fact use the same code, scope, table triplet of names as the primary index of the table; the remaining secondary indices will have their own unique triplet of names. This has been a point of confusion for some people because when they inspect the count of the “table metadata” for their logical table they may see double the number they expect depending on whether their logical table has a secondary index or not.

For a eosio::multi_index instance that has no secondary indices. The RAM bill for adding a table row to this logical table represented by the eosio::multi_index instance is the RAM bill for the primary index as well as possibly the additional RAM bill for the “table metadata” structure if this is the first table row of the logical table. So that is 112 bytes (or 224 bytes if this is the first table row) plus the number of bytes used in the serialization of the struct representing the table data. If the eosio::multi_index has secondary indices, then one can adjust what the RAM bill would be for adding a table row by also adding in the appropriate number of bytes depending on the secondary key type (discussed above).

RAM Bill for eosio.token Contract

As you already mentioned, calling eosio.token::open on an account that does not yet have a balance consumed 240 bytes. Let’s see why this is the case. The action adds a table row into the accounts table which only has no secondary indices and involves a struct with just a single asset field. This means the serialization of that struct takes only 16 bytes and so the RAM bill for adding that table row (assuming it is isn’t the first one for that code, scope, table triplet) should be 112 + 16 = 128 bytes. However, the scope used for this logical table is the owner of the tokens, and therefore this should be a unique code, scope, table triplet for each user. That means when opening a balance for an account that does not yet have a balance, or for that matter when sending tokens to an account that does not yet have a balance, the system will also charge the payer for the RAM of the “table metadata” structure. This adds an addition 112 bytes to the RAM bill which leads to the 240 byte total that you mentioned.

Also notice that the above explains why only 128 bytes are released when the RAM payer for the table row is changed to another account (or alternatively if the RAM locked up by a zero balance was freed via the eosio.token::close action). The remaining 112 bytes are used for the “table metadata” structure which is still paid for by the original sender of the funds to the account and will remain so until the account closes their balance.

3 Likes