NOTE: work in progress, submitted as unfinished, for the finished article please check the linked source. This will be left as it was submitted.
Motivations for Building a Blockchain from Scratch:
The VM (Virtual Machine) component in blockchain systems. Its main purpose is to execute the code that triggers state transitions within the blockchain. It acts as a computational engine, responsible for determining whether the code can run successfully or not.
Think of the VM as a machine that enforces the rules and restrictions of the blockchain. When code is executed by the VM, it checks if the code is allowed to run based on predefined conditions. If the code complies with the rules, the state of the blockchain is updated accordingly, reflecting the changes caused by the code execution.
However, if the code violates any of the predefined rules or runs out of gas (meaning it exceeds the allocated computational resources), the VM halts the execution. In such cases, the code may revert, meaning the changes made to the state are undone, and an error or exceptional condition is triggered.
For this part, the VM will the following attributes:
Note: In this context will be meaning Extended VM and not Ethereum VM. (Though it is building an Ethereum like virtual machine)
to write a simple interpreter that does simple arithmetic and bitwise logic operations is pretty straightforward
enum Bytecode {
// Define the bytecode instructions
Add,
Sub,
Mul,
Div,
Push(i32),
Halt,
}
fn interpreter(bytecode: Vec<Bytecode>, gas_limit: u32) {
let mut stack: Vec<i32> = Vec::new();
let mut pc: usize = 0;
let mut gas_left = gas_limit;
while pc < bytecode.len() && gas_left > 0 {
match bytecode[pc] {
Bytecode::Add => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a + b);
gas_left -= 1;
}
Bytecode::Sub => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a - b);
gas_left -= 1;
}
Bytecode::Mul => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a * b);
gas_left -= 1;
}
Bytecode::Div => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a / b);
gas_left -= 1;
}
Bytecode::Push(value) => {
stack.push(value);
gas_left -= 1;
}
Bytecode::Halt => {
break;
}
}
pc += 1;
}
if gas_left == 0 {
println!("Out of gas!");
} else if let Some(result) = stack.pop() {
println!("Result: {}", result);
} else {
println!("Stack is empty!");
}
}
fn compile(bytecode_str: &str) -> Vec<Bytecode> {
let bytecode = bytecode_str
.split(',')
.map(|opcode| match opcode.trim() {
"ADD" => Bytecode::Add,
"SUB" => Bytecode::Sub,
"MUL" => Bytecode::Mul,
"DIV" => Bytecode::Div,
"HALT" => Bytecode::Halt,
inst => {
let value = inst.replace("PUSH ", "");
if let Ok(num) = value.parse::<i32>() {
Bytecode::Push(num)
} else {
panic!("Invalid bytecode instruction: {}", value);
}
}
})
.collect();
bytecode
}
fn main() {
// Example bytecode as a string
let bytecode_str = "PUSH 5, PUSH 3, ADD, HALT";
let gas_limit = 100;
let bytecode = compile(bytecode_str);
interpreter(bytecode, gas_limit);
}
In this example we wrote a machine that allows you do do arithmetic operations: addition, sub, divisions, multiply without stack overflow or underflow in a manner that each operation is metered through the gas. This minimal machine does not have
Our blockchain is going to support the following operations. We go through and add support for each group as needed.
The VM instruction set offers most of the operations you might expect, including:
The operations: ADD, MUL, SUB, DIV, SDIV, MOD, SMOD common stack operations that most people are familiar, the only difference is that is these operations performed on 256 bit stack.
ADDMOD, MULMOD, KECCACK256 operations to support cryptograpy.
Common vm logic operations. Please check the source code for implementations details.
These are also common vm operations.
Common process flow operations. Check source code.
To implement the above operations we need to introduce and refactor our minimal applications and add the following
the looop
Operations will fall into the following categories stack operations
pub fn operation(
pc: &mut u64,
interpreter: &mut interpreter::Interpreter,
scope: &mut interpreter::ScopeContext,
) -> Result<Vec<u8>, Error> {
let x = match scope.stack.pop() {
Some(value) => value,
None => return Err(Error::StackUnderflow),
};
let y = match scope.stack.peek() {
Some(value) => value,
None => return Err(Error::StackUnderflow),
};
if let Some(result) = do_something_with_operands(x, y) {
*y = result;
Ok(Vec::new())
} else {
Err(Error::Overflow)
}
}
memory operation interpreterer.statedb operation (accessing world state) scope.contract scope.memory other parts that use the ScopeContext
In this implementation gas is static for all call, so this is not providing the right economic security model, and it's gameable. We update the gas implemenation in another part of this series.
the following operation will access the extended context
TODO