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
table name. The 64-bit integer key must be unique within the context of a particular
table triplet. Each
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
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
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
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
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
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.
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
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
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
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.