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

Persistent Storage Rent Paid by Code (Discussion) #78

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

Persistent Storage Rent Paid by Code (Discussion) #78

SergioDemianLerner opened this issue Jul 10, 2018 · 0 comments

Comments

@SergioDemianLerner
Copy link
Collaborator

When analyzing Persistent Storage Rent Paid by Code several problems arise:

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

  2. The payed 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 the rent-paying contract execution should 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)

  3. Still one more possibility to go through this path is to limit contract execution scheduling to short executions (gas limited). Still another problem arises: how to prevent massive number of contracts requesting scheduling 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.

  4. Suppose that contracts must pay for memory rent once a month. An owner of a name in the DNS contract would prefer to pay an annual fee, rather than worrying about a monthly fee. If names must reserve bitcoins to pay for gas, then they must know the gasPrice one year ahead of time. This adds unnecessary uncertainty.

There are several questions that must be answered:

  1. When to rise the Not-enough-Rent (NER) "exception".

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

The first question admits several possibilities:

a. forced NER checking and processing at certain blocks.

b. deadlines for pre-deposited bounties for hibernating the contract to become active.

The option (a) may suffer from too many processing at certain blocks, even if the NER checking deadlines are somehow randomized.

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.

S1) Periodic payments in BTC

Every time contract is created, a random rent-paying deadline based on blockhash is assigned in the future, preferably not before 12 months and not after 18 month. 1 month before the deadline, a programmed event is scheduled and its execution is forced. The event is executed with a fixed maximum gas. Also the gasPrice is fixed for the event one month before the event. (still another) problem with this approach is that once the deadline is known, there is an incentive to register a name just after the deadline (to prevent an early pay).

S2) Not-Enough-Rent (NER) checked when messages are processed

Rent is only be paid when a message is received. Every time a name balance is increased, the deadline checking code is executed, it deadline is too close (less than 6 months) the rent is paid in advance. This brings uncertainty (what if nobody remembers to pay in the last 6 months?).

S2B) Rent paid by users when they send a message to a contract

Users directly pay rents. If some time passes and the amount of gas specified by the message is not enough to pay the memory rent, then the contract is hibernated.

This has the drawback that if a contract stores a high amount of data, then quickly the rent may become too high for normal users to be willing to pay for them. For example, no new users will want to interact with the contract because they will be paying rent for memory they never used.

S3) Persistent Memory Cells are a Ledger

Every persistent cell has its own associated monetary balance and pubkey. The rent paying is done not by executing contract code, but forced by the protocol: every cell is scanned and cell with no balance are removed automatically. The contract must use centinel cells (with no balance) to detect a garbage-collection has occurred, and re-scan its memory to rebuild the necessary data indexes to continue working.

S4) Distributed Memory for rent democratization

S3 solution lines up with the idea that a contract could use distributed account memory instead of centralized memory (RSKIP01). In this case, the account would periodically pay for its own memory. If not payed, it will be garbage collected. This does not give a solution on how to pay a rent for Centralized memory: one possibility is that distributed memory pays a share of centralized memory. For example, if a contract has 10 Kbytes of centralized memory, and 100 Kbytes of decentralized memory, belonging to 100 different users, then each user pays the rent for 10.1 Kbytes of memory. This brings a new problem: what if I want to pay rent for some piece of data that I own, but not for some other. For example, I have 10 PlutoShares and 100 TetherUSD. Since PlutoShares are now worth zero, I don't want to pay rent for that space. A solution is to use different accounts to store different assets (this in turn requires maintaining different private keys for each). I could command my account to remove a certain section of my account memory before paying the rent. Another problem is that if any contract can use my account memory without my authorization, then why should I pay for that? The solution is that only the contracts that I enable should be able to. Therefore the platform needs a special command ENABLE_STORAGE / DISABLE_STORAGE to enable account memory use for a specified contract (or alternatively, the VM needs two more opcodes to do the same). Another problem is that if the rent is paid at a specific date, then the assets are worth less just before the pay-day, and more just afterwards: that's ugly. One solution is that doing anything with the contract that holds the distributed chunk of memory must pay the rent for the time it was unused, so transferring a TetherUSD would yield more gas fees if the TetherUSD were not used for a long time.

Let's take for example a DAO. If the shares are bearer-instruments, and we allow them to be transferred in peer to peer mode (without using the centralized memory), then that means that the centralized memory does not know who has the shares. Therefore it cannot pay dividends: users should call the issuer contract to collect dividends. Shares should be represented by tokens having a dividend-paid label. For example, a share would be a tuple (d,a), where d is the amount of dividends cycles it has received, and a is the amount. To split a share in two, the computation is (d,a) = (d,a1) + (d, a2) where a1+a2 = a. Two shares with different deivident cycles cannot be added: the only will less "d" value should have its dividends collected until it matches the "d" value of the other share term to add. The p2p memory system also means that transfer of tokens cannot be specially taxed by the main contract. Of course, if a share vanishes because the owner has not paid the memory rent, then the main contract has no way of knowing this, unless shares must be periodically registered with the main contract.

S5) External contracts pay for cells

Another bad option: we tag every a value in the persistent memory as EXTERNAL, which means that when the external contract pays rent, this cell is also paid. the value corresponds to the address of another contract. When this external contract dies, the pair is automatically erased. Internally this requires a contract to have a list of persistent memory cells pointing.

S6) Child Contracts

S1 + a twist seems to be the best solution. S1 has a drawback: a fully distributed contract REGISTRY that stores information that belongs to other users (such as a DNS), must pay rent for all its users in a centralized way. To solve this problem, we will encourage the use of master-child contracts: every user must have a child contract that stores the data required by the registry and the user that owns the record is responsible for maintaining its rent. However, even a single (a,b) pair is required to be stored in REGISTRY to locate the record.

One opcion: every child contract rent contributes to parent contract rent (accumulates gas). When a child contract C is created, the parent sets the externalMem property. To keep alive the contract C the user must pay for size(C)+externalMem bytes. Also parents should be able to write persistent memory of childs using two new opcodes.CSTORE and CLOAD.

All ideas regarding memory rent goes against one of the main goals of smart contracts that is immutability, which seems to be a pillar of no third party trust. Child contracts allow different unrelated parties to collaborate to maintain the rent of a single master contract.

If a smart contract solution is organized in several related contracts, the child contract idea does not allow the family of contracts to benefit from child rent. One possible solution is that rent is not automatically paid, but the child contract has a method PayRent that calls the PayRent method of the parent, who then re-distributes the rent to the remaining members of the family. Since the child contract code is chosen by the parent, he can create whatever rent distribution algorithm he desires. If this scheme is implemented, then there can’t be a DEPOSIT_RENT opcode that receives a contract address argument: all payments must be done by the contract code itself. Or better, there can be a contract flag that prevents external payments of contract rent. To avoid duplication of child contract code, we should implement opcodes of easy proxy calls.

To allow child contracts to be easily removed when parent contract dies, the child contract address could be built with the 20-byte parent contract address, plus 1 zero pad byte plus 4 bytes (DWORD) of child-addresses. Child addresses would be just the nonce of the parent contract on creation.

I don’t see why child contracts should have code if the parent contract can access child contract persistent memory. A variant is that child contracts do have code, and this code provides getters and setters to child persistent storage. This option is more "clean" in the sense it does not require two new opcodes. But also makes child contracts more expensive, when actually child contracts are used as lightweight isolated storage. Anyway, it’s possible to let the programmer decide which method he will use, and allow both CLOAD/CSTORE and child contract code.

CSTORE

Arguments:

CLOAD

Arguments:

Returns:

CREATE_CHILD (deprecated version)

Arguments: <in_size> <in_offs> <gas_val>

Creates a child contract. The child contract address is Hash(Parent-address || new-child)

CREATE_CHILD (new version)

Arguments: <in_size> <in_offs> <gas_val> (same as create)

Creates a child contract. The child contract address is Parent-address || 0 || parent-nonce

SET_FLAGS : new-bit ForbidExternalRentPayments (default 0 = false)

Chosen solution

Each persistent memory cell has an additional bit of information "survive". When a cell is marked to survive (survive=true) then when the next NER deadline arrives, the system tries to keep that cell for the next rent period. If the cell is not marked (survive=false) then when NER checking occurs, the cell is first cleared (no gas consumed for this) and then the NER checking occurs.

Contract persistent memory can be of any of three types:

  • immutable, paying 10 years in pre-deposit for each cell.

  • hibernable on unpaid-rent

  • killed on unpaid-rent.

If it is hibernable, if rent 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 parity contracts. This opcode could accept an argument (flags) of whether to hibernate code, data or both, but this was discarded for simplicity Data hibernation can always be achieved programmatically, by destroying data but keeping a hash in persistent memory. Internal hibernation freezes the contract until external wake up is performed. For simplicity, hibernation will always remove all contract code and memory. 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