Solana Internals Part 4
The Bank - A Key Component
January 30, 2022

Following Part 3: the TPU, this article elaborates on the bank module, a core component of Solana blockchain.

What Is a Bank?

The importance of the bank module cannot be overstated:

It manages the state of all accounts and programs, executes the on-chain programs, and tracks their progress.

At a high level, a bank relates to a block produced by a single leader and each bank (except for the genesis bank) points back to a parent bank.

The bank is the main entrypoint for processing verified transactions. In Bank::process_transactions, it creates an InvokeContext to process each transaction.

InvokeContext In Detail

The invoke_context.process_instruction is a key function that processes each instruction, verifies the called program has not misbehaved, maintains a cache to store compiled instructions, returns how many compute units were used, and so on.

It has a few parameters: the instruction_data and instruction_accounts , the program_indices (for retrieving the invoked program_id), the compute_units_consumed (for recording the compute units used for executing the instruction, initially 0), and timings (for the execution time info).

To invoke an instruction on a program, invoke_context.process_instruction first uses the program’s owner to load the program, and then calls the program’s entrypoint.

The owner of the called program is one of the following:

  • the native loader (which loads the built-in programs)
  • built-in programs (e.g., system instruction)

If it is the native loader (NativeLoader1111111111111111111111111111111), then the corresponding built-in program’s entrypoint process_instruction will be called:

Otherwise, the built-in program’s entrypoint process_instruction will be called:

The process_instruction function takes as input three parameters: first_instruction_account (the invoked program_id ), the instruction_data , and the invoke_context itself:

system_instruction.process_instruction

Consider system_instruction.process_instruction, it handles the following instructions:

The most frequently used instructions are CreateAccount , Transfer and Allocate .

bpf_loader.process_instruction

The bpf_loader.process_instruction function is used to execute user-deployed smart contracts (i.e., the BPF byte code):

The function calls process_instruction_common , which creates a BpfExecutor passing the program data and calls its execute function:

In BpfExecutor.execute, it creates a vm and executes the program by either vm.execute_program_jit or vm.execute_program_interpreted.

How Is the BPF Code Executed?

Importantly, the BPF byte code is not executed by the Linux kernel, but by a BPF virtual machine (EbpfVm).

By default, use_jit is false, and vm.execute_program_interpreted is used, i.e., BPF code is interpreted by the vm. This also means that Solana has a large potential to further improve performance, e.g. by executing the BPF code natively in the Linux kernel (though more technical details and security safeguard need to be fleshed out there).

The vm is EbpfVm defined in rbpf (an extended version of uBPF: a virtual machine and JIT compiler for eBPF programs).

Note that rbpf is not audited and it contains numerous unsafe Rust function blocks. Any errors in rbpf may cause severe vulnerabilities, such as Integer overflows and memory corruptions. See this post by BlockSec for an example.

Dealing With Cross Program Invocation (CPI)

When the invoked program calls another program through invoke or invoke_signed, that program will be loaded and its entry point will be called.

Internally, this is done by a syscall to sol_invoke_signed_rust, which will call invoke_context.process_instruction again.

The corresponding BPF instruction is ebpf::CALL_IMM (see vm.rs L939-L972)

The syscall.function is retrieved from the syscall_registry , which has been initialized with numerous built-in system calls such as sol_invoke_signed_c, sol_invoke_signed_rust, sol_create_program_address, sol_keccak256, etc.

A full list of registered system calls can be found in syscall.rs.

The transaction_context

After processing a CPI, the results (updates on all involved accounts) will be copied back to the caller:

The  InvokeContext has a transaction_context to track the current calling context, and to ensure that

  • the call depth is limited to max_invoke_depth set in the compute budget (max_invoke_depth: 4)
  • it disallows reentrancy unless caller is calling itself

Verify the Calling Program Hasn’t Misbehaved

The design of Solana for verifying an instruction call is similar to transactional memory: it executes the instruction first and then verifies the results to ensure that the invoked program has not misbehaved.

The bank is also responsible for verifying the results of an instruction and every CPI with respect to the accounting rules, a list of properties critical to Solana.

Depending on if the invocation level, it will call either verify or verify_and_update (which also updates the results if verified):

Important: The Accounting Rules

The bank maintains states of the instruction accounts before and after the instruction execution, and extensively checks rules:

  • It verifies an invariant that the total sum of all the lamports did not change:
  • It verifies all executable accounts have zero outstanding references:
  • It verifies that 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 owned by the program cannot debit the account:
  • The balance of read-only and executable accounts may not change:
  • Account data size cannot exceed a maximum length:
  • Only the owner of the account can change the size of the data:
  • 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 modifies rent_epoch:

The Bank Lifecycle

On a high level, the life cycle of a bank includes the following phases:

  1. Open: A new bank is created and transactions are applied to it until either the bank reached the tick count when the node is the leader for that slot, or the node has applied all transactions present in all entries in the slot.
  2. Committed: For a transaction, only if all instructions in the transaction succeed, the accounts are committed back to the bank and then the results are stored to the accounts store.
  3. Frozen: Once it is complete, the bank can then be frozen. After frozen, no more transactions can be applied or state changes made. At the frozen step, rent will be applied and various sysvar special accounts update to the new state of the system.
  4. Rooted: After frozen, and if the bank has had the appropriate number of votes on it, then it can become rooted. At this point, it will not be able to be removed from the chain and the state is finalized.

We will continue to introduce the architecture of Solana and its technical components in the next article.


About sec3 (Formerly Soteria)

sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with GitHub to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.

To learn more about sec3, please visit https://www.sec3.dev