Skip to content

Commit

Permalink
update compute budget docs
Browse files Browse the repository at this point in the history
  • Loading branch information
jackcmay committed Jun 22, 2022
1 parent 752c851 commit ed7a59f
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 123 deletions.
95 changes: 44 additions & 51 deletions docs/src/developing/programming-model/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,30 @@ The policy is as follows:

## 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](#transaction-wide-compute-budget).
To prevent abuse of computational resources, each transaction is allocated a
compute budget. The budget specifies a maximum number of compute units that a
transaction can consume, the costs associated with different types of operations
the transaction may perform, and operational bounds the transaction must adhere
to. As the transaction is processed compute units are consumed by its
instruction's programs performing operations such as executing BPF instructions,
calling syscalls, etc... When the transaction consumes its entire budget, or
exceeds a bound such as attempting a call stack that is too deep, the runtime
halts the transaction processing and returns an error.

The following operations incur a compute cost:

- Executing BPF instructions
- Passing data between programs
- 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.
For cross-program invocations, the instructions invoked inherit the budget of
their parent. If an invoked instruction consumes the transactions remaining
budget, or exceeds a bound, the entire invocation chain and the top level
transaction processing are halted.

The current [compute
budget](https://github.com/solana-labs/solana/blob/db32549c00a1b5370fcaf128981ad3323bbd9570/program-runtime/src/compute_budget.rs)
Expand All @@ -75,7 +77,7 @@ can be found in the Solana Program Runtime.
For example, if the current budget is:

```rust
max_units: 200,000,
max_units: 1,400,000,
log_u64_units: 100,
create_program address units: 1500,
invoke_units: 1000,
Expand All @@ -86,72 +88,63 @@ log_pubkey_units: 100,
...
```

Then the program
Then the transaction

- Could execute 200,000 BPF instructions, if it does nothing else.
- Could execute 1,400,000 BPF instructions, if it did 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
Since the compute budget is consumed incrementally as the transaction 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](developing/on-chain-programs/debugging.md#monitoring-compute-budget-consumption)
for more information.

A transaction may set the maximum number of compute units it is allowed to
consume by including a "request units"
[`ComputeBudgetInstruction`](https://github.com/solana-labs/solana/blob/db32549c00a1b5370fcaf128981ad3323bbd9570/sdk/src/compute_budget.rs#L39).
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.
consume and the compute unit price by including a `SetComputeUnitLimit` and a
`SetComputeUnitPrice`
[`ComputeBudgetInstruction`](https://github.com/solana-labs/solana/blob/db32549c00a1b5370fcaf128981ad3323bbd9570/sdk/src/compute_budget.rs#L39)
respectively.

If no `SetComputeUnitLimit` is provided the limit will be calculated as the
product of the number of instructions in the transaction (excluding the [Compute
Budget](#compute-budget) instructions) by the default per-instruction units,
which is currently 200k.

Note that a transaction's prioritization fee is calculated by multiplying the
number of compute units by the compute unit price (measured in micro-lamports)
set by the transaction via compute budget instructions. 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 actually 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
The `ComputeBudgetInstruction::set_compute_unit_limit` and `set_compute_unit_price` function scan be used to create
these instructions:

```rust
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](#compute-budget).

The transaction-wide compute budget applies the `max_units` cap to the entire
transaction rather than to each instruction within the transaction. The default
transaction-wide `max_units` will be calculated as the product of the number of
instructions in the transaction (excluding [Compute Budget](#compute-budget)
instructions) by the default per-instruction units, which is currently 200k.
During processing, the sum of the compute units used by each instruction in the
transaction must not exceed that value. This default value attempts to retain
existing behavior to avoid breaking clients. Transactions can request a specific
number of `max_units` via [Compute Budget](#compute-budget) instructions.
Clients should request only what they need; requesting the minimum amount of
units required to process the transaction will reduce overall transaction cost,
which may include a prioritization-fee charged for every compute unit.
```rust
let instruction = ComputeBudgetInstruction::set_compute_unit_price(1);
```

## New Features

As Solana 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. Solana supports a
mechanism called runtime features to facilitate the smooth adoption of changes.
coordinated between the various nodes of the cluster. If nodes do not
coordinate, then these changes can result in a break-down of consensus. Solana
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 Solana that will change behavior are
Expand Down
33 changes: 10 additions & 23 deletions docs/src/implemented-proposals/transaction-fees.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,18 @@
title: Deterministic Transaction Fees
---

Transactions currently include a fee field that indicates the maximum fee field a slot leader is permitted to charge to process a transaction. The cluster, on the other hand, agrees on a minimum fee. If the network is congested, the slot leader may prioritize the transactions offering higher fees. That means the client won't know how much was collected until the transaction is confirmed by the cluster and the remaining balance is checked. It smells of exactly what we dislike about Ethereum's "gas", non-determinism.

## Congestion-driven fees

Each validator uses _signatures per slot_ \(SPS\) to estimate network congestion and _SPS target_ to estimate the desired processing capacity of the cluster. The validator learns the SPS target from the genesis config, whereas it calculates SPS from recently processed transactions. The genesis config also defines a target `lamports_per_signature`, which is the fee to charge per signature when the cluster is operating at _SPS target_.

## Calculating fees

The client uses the JSON RPC API to query the cluster for the current fee parameters. Those parameters are tagged with a blockhash and remain valid until that blockhash is old enough to be rejected by the slot leader.

Before sending a transaction to the cluster, a client may submit the transaction and fee account data to an SDK module called the _fee calculator_. So long as the client's SDK version matches the slot leader's version, the client is assured that its account will be changed exactly the same number of lamports as returned by the fee calculator.
Before sending a transaction to the cluster, a client may query the network to
determine what the transaction's fee will be via the rpc request
[getFeeForMessage](clients/jsonrpc-api#getfeeformessage).

## Fee Parameters

In the first implementation of this design, the only fee parameter is `lamports_per_signature`. The more signatures the cluster needs to verify, the higher the fee. The exact number of lamports is determined by the ratio of SPS to the SPS target. At the end of each slot, the cluster lowers `lamports_per_signature` when SPS is below the target and raises it when above the target. The minimum value for `lamports_per_signature` is 50% of the target `lamports_per_signature` and the maximum value is 10x the target \`lamports_per_signature'

Future parameters might include:

- `lamports_per_pubkey` - cost to load an account
- `lamports_per_slot_distance` - higher cost to load very old accounts
- `lamports_per_byte` - cost per size of account loaded
- `lamports_per_bpf_instruction` - cost to run a program

## Attacks

### Hijacking the SPS Target

A group of validators can centralize the cluster if they can convince it to raise the SPS Target above a point where the rest of the validators can keep up. Raising the target will cause fees to drop, presumably creating more demand and therefore higher TPS. If the validator doesn't have hardware that can process that many transactions that fast, its confirmation votes will eventually get so long that the cluster will be forced to boot it.
The fee is based on the number of signatures in the transaction, the more
signatures a transaction contains, the higher the fee. In addition, a
transaction can specify an additional fee that determines how the transaction is
relatively prioritized against others. A transaction's prioritization fee is
calculated by multiplying the number of compute units by the compute unit price
(measured in micro-lamports) set by the transaction via compute budget
instructions.
13 changes: 1 addition & 12 deletions docs/src/proposals/comprehensive-compute-fees.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ the transaction. By calculating the total cost of the transaction, the runtime
can charge a more representative fee and make better transaction scheduling
decisions.

A fee will be calculated based on:
A fee could be calculated based on:

1. Number of signatures
- Fixed rate per signature
Expand Down Expand Up @@ -77,17 +77,6 @@ takes within a slot to process.

https://github.com/solana-labs/solana/issues/20511

### Transaction-wide compute caps

The current compute budget caps are independently applied to each instruction
within a transaction. This means the overall transaction cap varies depending on
how many instructions are in the transaction. To more accurately schedule a
transaction, the compute budget will be applied transaction-wide. One challenge
of the transaction-wide cap is that each instruction (program) can no longer
expect to be given an equal amount of compute units. Each instruction will be
given the remaining units left over after processing earlier instructions. This
will provide some additional tuning and composability challenges for developers.

### Requestable compute budget caps and heap sizes

The precompiled
Expand Down
43 changes: 6 additions & 37 deletions programs/bpf_loader/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ use {
feature_set::{
blake3_syscall_enabled, check_physical_overlapping, check_slice_translation_size,
curve25519_syscall_enabled, disable_fees_sysvar, executables_incur_cpi_data_cost,
fixed_memcpy_nonoverlapping_check, libsecp256k1_0_5_upgrade_enabled,
limit_secp256k1_recovery_id, prevent_calling_precompiles_as_programs,
quick_bail_on_panic, syscall_saturated_math, update_syscall_base_costs,
zk_token_sdk_enabled,
libsecp256k1_0_5_upgrade_enabled, limit_secp256k1_recovery_id,
prevent_calling_precompiles_as_programs, quick_bail_on_panic, syscall_saturated_math,
update_syscall_base_costs, zk_token_sdk_enabled,
},
hash::{Hasher, HASH_BYTES},
instruction::{
Expand Down Expand Up @@ -1321,14 +1320,6 @@ declare_syscall!(
}
);

/// This function is incorrect due to arithmetic overflow and only exists for
/// backwards compatibility. Instead use program_stubs::is_nonoverlapping.
#[allow(clippy::integer_arithmetic)]
fn check_overlapping_do_not_use(src_addr: u64, dst_addr: u64, n: u64) -> bool {
(src_addr <= dst_addr && src_addr + n > dst_addr)
|| (dst_addr <= src_addr && dst_addr + n > src_addr)
}

fn mem_op_consume<'a, 'b>(
invoke_context: &Ref<&'a mut InvokeContext<'b>>,
n: u64,
Expand Down Expand Up @@ -1378,24 +1369,13 @@ declare_syscall!(
question_mark!(invoke_context.get_compute_meter().consume(cost), result);
}

let use_fixed_nonoverlapping_check = invoke_context
.feature_set
.is_active(&fixed_memcpy_nonoverlapping_check::id());
let do_check_physical_overlapping = invoke_context
.feature_set
.is_active(&check_physical_overlapping::id());

#[allow(clippy::collapsible_else_if)]
if use_fixed_nonoverlapping_check {
if !is_nonoverlapping(src_addr, dst_addr, n) {
*result = Err(SyscallError::CopyOverlapping.into());
return;
}
} else {
if check_overlapping_do_not_use(src_addr, dst_addr, n) {
*result = Err(SyscallError::CopyOverlapping.into());
return;
}
if !is_nonoverlapping(src_addr, dst_addr, n) {
*result = Err(SyscallError::CopyOverlapping.into());
return;
}

if !update_syscall_base_costs {
Expand Down Expand Up @@ -4795,17 +4775,6 @@ mod tests {
}
}

#[test]
fn test_overlapping() {
assert!(!check_overlapping_do_not_use(10, 7, 3));
assert!(check_overlapping_do_not_use(10, 8, 3));
assert!(check_overlapping_do_not_use(10, 9, 3));
assert!(check_overlapping_do_not_use(10, 10, 3));
assert!(check_overlapping_do_not_use(10, 11, 3));
assert!(check_overlapping_do_not_use(10, 12, 3));
assert!(!check_overlapping_do_not_use(10, 13, 3));
}

fn call_program_address_common(
seeds: &[&[u8]],
program_id: &Pubkey,
Expand Down

0 comments on commit ed7a59f

Please sign in to comment.