Following Part 3: the TPU, this article elaborates on the bank module, a core component of Solana blockchain.
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.
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:
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:
Consider system_instruction.process_instruction, it handles the following instructions:
The most frequently used instructions are CreateAccount , Transfer and Allocate .
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.
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.
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.
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 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):
The bank maintains states of the instruction accounts before and after the instruction execution, and extensively checks rules:
On a high level, the life cycle of a bank includes the following phases:
We will continue to introduce the architecture of Solana and its technical components in the next article.
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