Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Efficient Persistent Storage Rent Discussion #79

Open
SergioDemianLerner opened this issue Jul 10, 2018 · 0 comments
Open

Efficient Persistent Storage Rent Discussion #79

SergioDemianLerner opened this issue Jul 10, 2018 · 0 comments

Comments

@SergioDemianLerner
Copy link
Collaborator

When evaluating Persistent Storage Rent based on rent pre-deposit several problems arise:

  1. The the rent-paying contract execution could be scheduled using a crontab-like method. But one of the design choices of RSK/Ethereum is to avoid crontab-like scheduling because the CPU consumed by contract execution has a direct effect on block propagation, so the crontab system can be used to perform a DoS on certain miners (e.g. by using an expensive computation which the attacker knows the result, but not the honest miner)

  2. Since a contract cannot schedule an action at a specific time, triggering the rent-paying code would need to be done from a message coming from the outside world, before the rent deadline comes.

  3. The paid amount would be specified in gas, so the gasPrice applies. A contract cannot easily determine the adequate gasPrice to be paid unless the price is semi-fixed. Even if the minimum gas price is published by miners, miners are not forced to accept the rent-paying contract execution, not any other transaction. Therefore the period to pay for the contract rent should be long enough so no single miner can censor it.

  4. To prevent further censorship, the payment should be done by the execution of an opcode, and not by an explicit kind of transaction, so censorship carries the cost of the halting-problem.

  5. If deterministic deadlines are set some fixed time after the contract creation time, then the simultaneous creation of multiple contracts in the same block can lead to a high number of contracts requesting deadline checks at the same time. One possibility is that rent-deadline events are chosen randomly at contract creation time, so the event is not previsible, and an attacker cannot create thousands of contracts with the same planned deadline.

  6. Short deadlines intervals may be fine for long-time savings accounts, but may be overkill for other kinds of contracts. Monthly payments seems too add too much pressure on users. For example, owner of domain names usually prefer to pay an annual fee, rather than worrying about a monthly fee. Also short deadlines adds uncertainty because the gas price may vary, preventing users to plan ahead of time, and bringing unnecessary uncertainty.

  7. In case of short contracts and accounts, it’s suspected that the frequent execution of contract rent payments consumes computing resources that are more expensive than the micro-rent that is being paid.

  8. If the rent is a micro-transaction, the fees for a full transaction (21K gas) is an huge overhead, therefore it would be preferable to pay the rent re-using another transaction, saving the space and computation time of a single signature. One way to allow this is using a transaction with embedded contract code: in this way several rents can be paid. Another approach is by using multi-output transactions: a transaction that has a single input but different outputs. Still another option is that a transaction from sender x can pay the rent for address x using a single new field in the transaction.

  9. We’ll define the Not-enough-Rent (NER) "exception" as the event when it is detected that the contract has not paid its rent in time. The crucial question is therefore when to rise the NER. Also there other additional options:

  • Rent payment is direct or via a pre-deposit.

  • The NER is automatically detected or there are external parties trigger the deadline check via HIBERNATE.

The question of when to rise NER admits several possibilities:

a. forced NER checking and processing at certain blocks heights (deadlines) for each contract

b. forced NER checking of random contracts on each block, the contracts to check being a subset pseudo-randomly derived from the previous block hash and current block timestamp.

c. unforced NER. Each account has a deadline and a pre-deposited bounties for hibernating the contract on NER.

d. forced NER on postponed deadline.

The option (a) may suffer from too many processing at certain blocks, even if the NER checking deadlines are somehow randomized. If deadlines are randomized, then there exists the problem of storing the deadlines in a data structure that allows easily retrieving the closest deadlines. A heap or priority queue may do this, but this structure must be reorganized after each deadline arrives and a new deadline is created. It would be better if one could use a cyclic data structure: one which allows the detection of each deadline without modification. Such data structure is a closed double-linked-list. Each node stores the contract creation block number and the contract address. A head pointer points to the next block number such in NODE(head).creationHeigh is the closest to the next block number modulo 777600 (corresponding to 90 days at 10/seconds blocks). When the linked list is scanned, if a node points to a contract address that does not exists, then the node in the list is automatically removed. Adding this functionality is described in [RSKIP17], and adds another layer of complexity. Another complication is that transaction execution parallelization [RSKIP04] prevents several threads accessing writing to the same data structure for storing deadlines.

Note that the new "collapsing" system for hibernation does not allow a contract to detect if another contract exists.

The option (b) seems good, but the effectiveness depends on the ratio between accounts checked (C) per block and total number of accounts (T). If the total number of accounts is T=16M, and every block checks C=512 accounts, then the probability of a NER-account to be detected per block is 2^(T/C) = 2^-15 ~= 32K. Every day there are 8K blocks, so the probability of a contract to resist 3 days while being in NER state is (1-2^-15)^24000 ~= 0.48. So there is a 50% chance for an unscrupulous speculator to go undetected for 3 days.One possible variant is to make C dependant on T, such as C=Max(T/V,64). It must be taken into account that checking C accounts has a cost of retrieving the account from disk. If it takes 5 msec to do it, then checking such block will take 2.5 seconds more, which seems too high.Therefore it seems better that each contract has a clear deadline.

Regarding the question 2, direct rent paying is undesired because it puts the responsibility of the payment on a new user that may not have any stake in the contract.

This is a list of the ideas that were evaluated (and most of them discarded) in the process of selecting a solution.

The option (d) is interesting because it’s simpler. Every time a contract processes a message, the gas from the pre-deposit is consumed equivalent to the memory consumed in the last inactivity interval, and the deadline is postponed certain amount of time. However there are two problems with this approach: it allows memory gas arbitration (one can pre-deposit rent for 100 years), and the deadline data structure (which is central) must be updated every time a contract receive a message, adding a lot of work to transaction processing.

Other solutions with similar effects

In a following section we show that space consumption assuming the Ethereum post-EIP150 fork cost for storage operations is generally not a problem. This is true for all possible assumptions for a medium to store contract memory, such as HDD, SSD and RAM. However it is true that future parties that become full nodes must pay the price to store the state in order to be able to verify transactions. It seems unfair that previous miners/full nodes collect fees for future miners. One way to achieve a similar (although not exact same) result is by setting apart a percentage of mining fees for miners in a distant future. For instance every fee collected could be split in 50% for this year miners, 25% for second year miners, 12.5% for third year miners, etc. If there are N blocks per year, and p percentage is taken from the revenue por for miners in each block, then p must be such (1-p)^N=0.5. For blocks every 10 seconds, N=3153600, then (1-p)=0,9999997802. This however means that until the first year elapses, miners will receive less than 50% of what they would expect. Also this is not entirely fair, since future miners can mine only empty blocks to collect the storage subsidy, but do not actually store the state. A variant is that during the first year, the percentage p is replaced by a higher percentage p’, and p’ decreases very block until it reaches p. For instance, p’ starts with p(0)=p’=1, and p(n)=p(n-1)$*$p.

The difference between delaying fees and storage rent are the following:

  • Delaying fees does not account for variations in memory pricings. Variation in pricing allows the network adapt to real costs. Because transaction fees are always tied to current fee price, and the platform does not allow users to acquire gas in advance, the future usability of the platform already depend on the future prices, even if contract storage did not. Therefore, making contract storage still dependent on future prices does bring a fundamental economic change.

  • Storage rent can recover space from lost keys or spam attacks, where delaying transactions fees cannot.

Rent payment period

There are two types of deadlines that could be used: storage size dependant deadlines or periodic deadlines, or an hybrid of the two. An example of the first is fixing an amount of gas use for memory (rentNext maximum) and forcing the deadline if a certain amount is surpassed. This means that the period depends on the memory consumed. This has the benefit that contracts that use very little memory are not forced to pay micro-fees.

For example, a contract using 1 Kilobyte of memory (code+data), would need to pay 2400 gas units for a lifetime use, or 240 per year. But 240 gas units represents 80 times less than the cost of the transaction that pays that micropayment. Therefore the overhead is overkill. For a rent payment per year, a platform storing 50M accounts would use 1 payment per second just to keep accounts active.

Therefore the storage rent system becomes wasteful if based on periodic micropayments. However periodic payments have the advantage that the data structure required to handle deadlines is a simple double-linked list, while data size dependant payments would require a more complex priority queue which must be updated for every transaction call.

Private and public AOCs

One solution is that account/contracts (AOCs) are marked as private or public. Private contracts pay the rents automatically from each transaction that interacts with it. Accounts would be always private. Private AOCs would have a periodic deadline checking code, once a year, but would not require the use of DEPOSIT_RENT. The opcode NEXTRENT would return the rent required to pay up to that transaction.

Public AOCs would have the periodic deadline, but would use DEPOSIT_RENT system described.

A variant is to make accounts the only private AOCs, and contracts always public. This can reduce additional space used for accounts: the system can have a single constant GasPerBlock representing the overall space cost of an account, and consume GasPerBlock$*$(blockTime-lastUseTime) gas for each transaction. The only additional field required is lastUseTime. Note that the amount to be paid if there is a single transaction per day would be around 2 units of gas. Therefore the whole computation seems a high overhead for such micropayment. GasPerBlock being a power of two will slightly reduce the cost.

Another possibility is to assume that a single transaction (21K gas) or a CALL (400 gas) can subsidize account storage for a whole year. Every time an account receives or sends bitcoins the lastUseTime is updated. Only if a year passed without update the account can be hibernated. However this opens up the possibility of an attacker keeping active millions of contracts at low cost by running code that calls all inactive accounts in a loop. To prevent this attack, restrictions on the type of operations that affect lastUseTime can be set. For example, it can be restricted to transaction originating from the account, which limits them to operations executed by the account owner.

Still another possibility is to add a single bit per account yearRentPaid, set to false at start, and set a deadline every year to check which accounts have not paid a constant value per year, and clear all bits for the following year. But scanning all accounts (e.g. 30M accounts) may require loading the whole world state into RAM to prevent so many disk accesses. 30M accounts would require 8 GBytes of data to be loaded, so it’s again overkill.

One possibility is that accounts (or AOCs consuming less than 1 Kbyte of memory) will not pay storage rent. Therefore storage rent prevents an attacker from requesting a high amount of memory for a single contract, but does not prevent an attacker acquiring many small-sized contracts.

A problem is that if blocks are not saturated of gas use, then miners may use the gas left to pay rents for contracts (and offer this a service). The fact that RSK has a minimal gasPrice and we are planning to forward 90% of the fees collected in a block to the next blocks partially prevents this, since the miner will be having a 10% discount on the minimum price, but not zero cost.

(Better: why the checks about page memories are not done when the contract is CREATED????)

Chosen solution

Account persistence can be of a single type:

  • hibernable un unpaid rent. Rent paid automatically on sends.

Contract persistence can be of two types:

  • hibernable on unpaid-rent

  • killed on unpaid-rent.

If it is hibernable, if rent deposited is not enough, all the memory (contract+persistent mem) collapses into a single hash. To bring the contract alive again, a message paying the wakeup fee, containing all missing data must be sent.

A new opcode HIBERNATE is added for self-inflicted or hibernation of 3rd party contracts. Hibernation freezes the contract until external wake up is performed. Self-hibernation can be used by contracts to sleep an amount of time, since no rent is paid during hibernation time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant