Skip to main content

Runtime

Capability of Programs#

The runtime only permits the owner program to debit the account or modify its data. The program then defines additional rules for whether the client can modify accounts it owns. In the case of the System program, it allows users to transfer lamports by recognizing transaction signatures. If it sees the client signed the transaction using the keypair's private key, it knows the client authorized the token transfer.

In other words, the entire set of accounts owned by a given program can be regarded as a key-value store, where a key is the account address and value is program-specific arbitrary binary data. A program author can decide how to manage the program's whole state, possibly as many accounts.

After the runtime executes each of the transaction's instructions, it uses the account metadata to verify that the access policy was not violated. If a program violates the policy, the runtime discards all account changes made by all instructions in the transaction, and marks the transaction as failed.

Policy#

After a program has processed an instruction, the runtime verifies that the program only performed operations it was permitted to, and that the results adhere to the runtime policy.

The policy is as follows:

  • Only the owner of the account may change owner.
    • And only if the account is writable.
    • And only if the account is not executable.
    • And only if the data is zero-initialized or empty.
  • An account not assigned to the program cannot have its balance decrease.
  • The balance of read-only and executable accounts may not change.
  • Only the system program can change the size of the data and only if the system program owns the account.
  • Only the owner may change account data.
    • And if the account is writable.
    • And if the account is not executable.
  • Executable is one-way (false->true) and only the account owner may set it.
  • No one can make modifications to the rent_epoch associated with this account.

Compute Budget#

To prevent a program from abusing computation resources, each instruction in a transaction is given a compute budget. The budget consists of computation units that are consumed as the program performs various operations and bounds that the program may not exceed. When the program consumes its entire budget or exceeds a bound, the runtime halts the program and returns an error.

Note: The compute budget currently applies per-instruction, but is moving toward a per-transaction model. For more information see Transaction-wide Compute Budget.

The following operations incur a compute cost:

  • Executing BPF instructions
  • Calling system calls
    • logging
    • creating program addresses
    • cross-program invocations
    • ...

For cross-program invocations, the programs invoked inherit the budget of their parent. If an invoked program consumes the budget or exceeds a bound, the entire invocation chain and the parent are halted.

The current compute budget can be found in the Renec SDK.

For example, if the current budget is:

max_units: 200,000,
log_u64_units: 100,
create_program address units: 1500,
invoke_units: 1000,
max_invoke_depth: 4,
max_call_depth: 64,
stack_frame_size: 4096,
log_pubkey_units: 100,
...

Then the program

  • Could execute 200,000 BPF instructions, if it does nothing else.
  • Cannot exceed 4k of stack usage.
  • Cannot exceed a BPF call depth of 64.
  • Cannot exceed 4 levels of cross-program invocations.

Since the compute budget is consumed incrementally as the program executes, the total budget consumption will be a combination of the various costs of the operations it performs.

At runtime a program may log how much of the compute budget remains. See debugging for more information.

A transaction may set the maximum number of compute units it is allowed to consume by including a "request units" ComputeBudgetInstruction. Note that a transaction's prioritization fee is calculated from multiplying the number of compute units requested by the compute unit price (measured in micro-lamports) set by the transaction. So transactions should request the minimum amount of compute units required for execution to minimize fees. Also note that fees are not adjusted when the number of requested compute units exceeds the number of compute units consumed by an executed transaction.

Compute Budget instructions don't require any accounts and don't consume any compute units to process. Transactions can only contain one of each type of compute budget instruction, duplicate types will result in an error.

The ComputeBudgetInstruction::set_compute_unit_limit function can be used to create these instructions:

let instruction = ComputeBudgetInstruction::set_compute_unit_limit(300_000);

Transaction-wide Compute Budget#

Transactions are processed as a single entity and are the primary unit of block scheduling. In order to facilitate better block scheduling and account for the computational cost of each transaction, the compute budget is moving to a transaction-wide budget rather than per-instruction.

For information on what the compute budget is and how it is applied see Compute Budget.

With a transaction-wide compute budget the max_units cap is applied to the entire transaction rather than to each instruction within the transaction. The default number of maximum units allowed to each transaction is a default value per instruction in the transaction.The default value per instruction matches the existing per-instruction cap to avoid breaking existing client behavior.

There are a lot of uses cases that require more than 200k units transaction-wide. To enable these uses cases transactions can include a `ComputeBudgetInstruction requesting a higher compute unit cap. Higher compute caps will be charged higher fees.

Compute Budget instructions don't require any accounts and must lie in the first 3 instructions of a transaction otherwise they will be ignored.

The ComputeBudgetInstruction::request_units function can be used to crate these instructions:

let instruction = ComputeBudgetInstruction::request_units(300_000);

New Features#

As Renec evolves, new features or patches may be introduced that changes the behavior of the cluster and how programs run. Changes in behavior must be coordinated between the various nodes of the cluster, if nodes do not coordinate then these changes can result in a break-down of consensus. Renec supports a mechanism called runtime features to facilitate the smooth adoption of changes.

Runtime features are epoch coordinated events where one or more behavior changes to the cluster will occur. New changes to Renec that will change behavior are wrapped with feature gates and disabled by default. The Renec tools are then used to activate a feature, which marks it pending, once marked pending the feature will be activated at the next epoch.

To determine which features are activated use the Renec command-line tools:

renec feature status

If you encounter problems, first ensure that the Solana tools version you are using match the version returned by solana cluster-version. If they do not match, install the correct tool suite.