Storing State between Transactions
If the program needs to store state between transactions, it does so using accounts. Accounts are similar to files in operating systems such as Linux. Like a file, an account may hold arbitrary data and that data persists beyond the lifetime of a program. Also like a file, an account includes metadata that tells the runtime who is allowed to access the data and how.
Unlike a file, the account includes metadata for the lifetime of the file. That lifetime is expressed in "tokens", which is a number of fractional native tokens, called lamports. Accounts are held in validator memory and pay "rent" to stay there. Each validator periodically scans all accounts and collects rent. Any account that drops to zero lamports is purged. Accounts can also be marked rent-exemption if they contain a sufficient number of lamports.
In the same way that a Linux user uses a path to look up a file, a Velas client uses an address to look up an account. The address is a 256-bit public key.
Transactions may include digital signatures corresponding to the accounts' public keys referenced by the transaction. When a corresponding digital signature is present it signifies that the holder of the account's private key signed and thus "authorized" the transaction and the account is then referred to as a signer. Whether an account is a signer or not is communicated to the program as part of the account's metadata. Programs can then use that information to make authority decisions.
Transactions can indicate that some of the accounts it references be treated as read-only accounts in order to enable parallel account processing between transactions. The runtime permits read-only accounts to be read concurrently by multiple programs. If a program attempts to modify a read-only account, the transaction is rejected by the runtime.
If an account is marked "executable" in its metadata then it is considered a program which can be executed by including the account's public key an instruction's program id. Accounts are marked as executable during a successful program deployment process by the loader that owns the account. For example, during BPF program deployment, once the loader has determined that the BPF bytecode in the account's data is valid, the loader permanently marks the program account as executable. Once executable, the runtime enforces that the account's data (the program) is immutable.
To create an account a client generates a keypair and registers its public key using the
SystemProgram::CreateAccount instruction with preallocated a fixed storage size in bytes. The current maximum size of an account's data is 10 megabytes.
An account address can be any arbitrary 256 bit value, and there are mechanisms for advanced users to create derived addresses (
Accounts that have never been created via the system program can also be passed to programs. When an instruction references an account that hasn't been previously created the program will be passed an account that is owned by the system program, has zero lamports, and zero data. But, the account will reflect whether it is a signer of the transaction or not and therefore can be used as an authority. Authorities in this context convey to the program that the holder of the private key associated with the account's public key signed the transaction. The account's public key may be known to the program or recorded in another account and signify some kind of ownership or authority over an asset or operation the program controls or performs.
Ownership and Assignment to Programs
A created account is initialized to be owned by a built-in program called the System program and is called a system account aptly. An account includes "owner" metadata. The owner is a program id. The runtime grants the program write access to the account if its id matches the owner. For the case of the System program, the runtime allows clients to transfer lamports and importantly assign account ownership, meaning changing owner to different program id. If an account is not owned by a program, the program is only permitted to read its data and credit the account.
Verifying validity of unmodified, reference-only accounts
For security purposes, it is recommended that programs check the validity of any account it reads but does not modify.
The security model enforces that an account's data can only be modified by the account's
Owner program. Doing so allows the program to trust that the data passed to them via accounts they own will be in a known and valid state. The runtime enforces this by rejecting any transaction containing a program that attempts to write to an account it does not own. But, there are also cases where a program may merely read an account they think they own and assume the data has only been written by themselves and thus is valid. But anyone can issues instructions to a program, and the runtime does not know that those accounts are expected to be owned by the program. Therefore a malicious user could create accounts with arbitrary data and then pass these accounts to the program in the place of a valid account. The arbitrary data could be crafted in a way that leads to unexpected or harmful program behavior.
To check an account's validity, the program should either check the account's address against a known value or check that the account is indeed owned correctly (usually owned by the program itself).
One example is when programs read a sysvar. Unless the program checks the address or owner, it's impossible to be sure whether it's a real and valid sysvar merely by successful deserialization. Accordingly, the Velas SDK checks the sysvar's validity during deserialization.
If the program always modifies the account in question, the address/owner check isn't required because modifying an unowned (could be the malicious account with the wrong owner) will be rejected by the runtime, and the containing transaction will be thrown out.
Keeping accounts alive on Velas incurs a storage cost called rent because the cluster must actively maintain the data to process any future transactions on it. This is different from Bitcoin and Ethereum, where storing accounts doesn't incur any costs.
The rent is debited from an account's balance by the runtime upon the first access (including the initial account creation) in the current epoch by transactions or once per an epoch if there are no transactions. The fee is currently a fixed rate, measured in bytes-times-epochs. The fee may change in the future.
For the sake of simple rent calculation, rent is always collected for a single, full epoch. Rent is not pro-rated, meaning there are neither fees nor refunds for partial epochs. This means that, on account creation, the first rent collected isn't for the current partial epoch, but collected up front for the next full epoch. Subsequent rent collections are for further future epochs. On the other end, if the balance of an already-rent-collected account drops below another rent fee mid-epoch, the account will continue to exist through the current epoch and be purged immediately at the start of the upcoming epoch.
Accounts can be exempt from paying rent if they maintain a minimum balance. This rent-exemption is described below.
Calculation of rent
Note: The rent rate can change in the future.
As of writing, the fixed rent fee is 19.055441478439427 lamports per byte-epoch on the testnet and mainnet-beta clusters. An epoch is targeted to be 2 days (For devnet, the rent fee is 0.3608183131797095 lamports per byte-epoch with its 54m36s-long epoch).
This value is calculated to target 0.01 VLX per mebibyte-day (exactly matching to 3.56 VLX per mebibyte-year):
And rent calculation is done with the
f64 precision and the final result is truncated to
u64 in lamports.
The rent calculation includes account metadata (address, owner, lamports, etc) in the size of an account. Therefore the smallest an account can be for rent calculations is 128 bytes.
For example, an account is created with the initial transfer of 10,000 lamports and no additional data. Rent is immediately debited from it on creation, resulting in a balance of 7,561 lamports:
The account balance will be reduced to 5,122 lamports at the next epoch even if there is no activity:
Accordingly, a minimum-size account will be immediately removed after creation if the transferred lamports are less than or equal to 2,439.
Alternatively, an account can be made entirely exempt from rent collection by depositing at least 2 years-worth of rent. This is checked every time an account's balance is reduced and rent is immediately debited once the balance goes below the minimum amount.
Program executable accounts are required by the runtime to be rent-exempt to avoid being purged.
Note: Use the
getMinimumBalanceForRentExemption RPC endpoint to calculate the minimum balance for a particular account size. The following calculation is illustrative only.
For example, a program executable with the size of 15,000 bytes requires a balance of 105,290,880 lamports (=~ 0.105 VLX) to be rent-exempt:
Rent can also be estimated via the
velas rent CLI subcommand
Note: Rest assured that, should the storage rent rate need to be increased at some point in the future, steps will be taken to ensure that accounts that are rent-exempt before the increase will remain rent-exempt afterwards.