diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index c0acb52..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(make test:*)", - "Bash(make:*)", - "Bash(cargo test:*)", - "Bash(cargo:*)", - "Bash(rm:*)", - "Bash(make:*)", - "Bash(grep:*)", - "Bash(timeout 10 cargo test -p examples --test program_exec)", - "Bash(RUST_BACKTRACE=0 cargo test -p examples --test program_exec --nocapture)", - "Bash(RUST_BACKTRACE=0 cargo test -p examples --test program_exec -- --nocapture)", - "Bash(timeout 30 make all)", - "Bash(timeout 30s cargo test -p examples)", - "Bash(mv:*)", - "WebSearch", - "Bash(find:*)", - "Bash(echo $?)", - "Bash(echo \"Exit code: $?\")", - "Bash(git add:*)", - "Bash(git push:*)", - "Bash(RUST_LOG=debug cargo test -p examples -- --test-threads=1 --nocapture)", - "Bash(timeout 5 cargo test -p examples test_entrypoint_function)", - "Bash(readelf:*)", - "Bash(objdump:*)", - "Bash(RUST_BACKTRACE=1 cargo test -p examples test_entrypoint_function)", - "Bash(rustc:*)", - "Read(//Users/alonmuroch/Desktop/**)", - "Bash(timeout 5 cargo test -p examples test_entrypoint_function -- --nocapture)", - "Bash(git commit:*)", - "Bash(utils/binary_comparison/target/release/binary_comparison:*)", - "Bash(RUST_LOG=debug ./target/debug/deps/program_exec-7efd2a542591705b --nocapture)", - "Bash(./target/release/binary_comparison:*)", - "Read(//private/tmp/**)", - "Read(//Users/alonmuroch/**)", - "Bash(timeout 10 make all)", - "Read(//tmp/**)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2da6768..c24e220 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,9 @@ no_llvm_build # Created by nix dev shell / .envrc src/tools/nix-dev-shell/flake.lock +# Project outputs +crates/os/bin/ + ## ICE reports rustc-ice-*.txt diff --git a/Cargo.lock b/Cargo.lock index 3928bc8..4efb36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,13 +3,12 @@ version = 4 [[package]] -name = "avm" +name = "a_tests" version = "0.1.0" dependencies = [ "compiler", - "hex", + "goblin 0.8.2", "state", - "storage", "types", "vm", ] @@ -35,17 +34,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bootloader" +version = "0.1.0" +dependencies = [ + "clibc", + "compiler", + "goblin 0.10.0", + "state", + "types", + "vm", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "clibc" +version = "0.1.0" +dependencies = [ + "state", + "types", +] + [[package]] name = "compiler" version = "0.1.0" dependencies = [ - "goblin", + "goblin 0.10.0", ] [[package]] @@ -144,15 +163,9 @@ dependencies = [ name = "examples" version = "0.1.0" dependencies = [ - "avm", - "compiler", + "clibc", "k256", - "once_cell", - "program", - "serde_json", "sha2", - "state", - "types", ] [[package]] @@ -176,6 +189,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll 0.12.0", +] + [[package]] name = "goblin" version = "0.10.0" @@ -184,7 +208,7 @@ checksum = "0e961b33649994dcf69303af6b3a332c1228549e604d455d61ec5d2ab5e68d3a" dependencies = [ "log", "plain", - "scroll", + "scroll 0.13.0", ] [[package]] @@ -198,12 +222,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" @@ -213,12 +231,6 @@ dependencies = [ "digest", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "k256" version = "0.13.4" @@ -231,6 +243,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "kernel" +version = "0.1.0" +dependencies = [ + "clibc", + "state", + "types", +] + [[package]] name = "libc" version = "0.2.175" @@ -243,18 +264,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - [[package]] name = "pkcs8" version = "0.10.2" @@ -280,13 +289,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "program" -version = "0.1.0" -dependencies = [ - "types", -] - [[package]] name = "quote" version = "1.0.40" @@ -313,10 +315,13 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.20" +name = "scroll" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive 0.12.1", +] [[package]] name = "scroll" @@ -324,7 +329,18 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" dependencies = [ - "scroll_derive", + "scroll_derive 0.13.0", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -352,38 +368,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - [[package]] name = "sha2" version = "0.10.9" @@ -419,8 +403,6 @@ dependencies = [ name = "state" version = "0.1.0" dependencies = [ - "hex", - "storage", "types", ] @@ -473,7 +455,6 @@ version = "0.1.0" dependencies = [ "compiler", "state", - "storage", "types", ] diff --git a/Cargo.toml b/Cargo.toml index 782d4be..56c0260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,15 @@ [workspace] members = [ - "crates/avm", "crates/compiler", "crates/examples", - "crates/program", + "crates/bootloader", + "crates/kernel", + "crates/clibc", "crates/state", "crates/storage", "crates/types", "crates/vm", + "aTester", ] [profile.release] diff --git a/Makefile b/Makefile index 71edb5c..f26a250 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,31 @@ .PHONY: all -all: clean program examples test utils summary +# Nightly cargo for avm32 builds (used for kernel ELF and examples). +CARGO_NIGHTLY ?= cargo +nightly-aarch64-apple-darwin +AVM32 := $(CARGO_NIGHTLY) run -p compiler --bin avm32 -- +KERNEL_MANIFEST := crates/kernel/Cargo.toml +KERNEL_OUT_DIR := crates/bootloader/bin +KERNEL_BINS := $(shell awk '/\[\[bin\]\]/{inbin=1;next} inbin && /name =/{gsub(/"/,"",$$3); print $$3; inbin=0}' $(KERNEL_MANIFEST)) +KERNEL_TEST_BINS := $(filter-out kernel,$(KERNEL_BINS)) + +all: clean examples test utils summary .PHONY: run_examples +.PHONY: kernel + +kernel: + @echo "=== Building kernel ELF ===" + @mkdir -p $(KERNEL_OUT_DIR) + @$(AVM32) all --bin kernel --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/main.rs + @echo "=== Building kernel test ELFs ===" + @$(foreach bin,$(KERNEL_TEST_BINS),$(AVM32) all --bin $(bin) --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/memory/tests/$(patsubst kernel_%,%,$(bin)).rs;) run_examples: @echo "=== Building example programs ===" RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples - @echo "=== Running example crate tests ===" - cd crates/examples && RUSTFLAGS="-Awarnings" cargo test -- --nocapture + @$(MAKE) kernel + @echo "=== Running aTester example tests ===" + cargo test -p a_tests --test examples -- --nocapture @echo "=== Example programs build and tests complete ===" clean: @@ -18,12 +35,6 @@ clean: @cd utils/binary_comparison && $(MAKE) clean > /dev/null 2>&1 || true @echo "=== Clean complete ===" -program: - @echo "=== Building program ===" - cargo clean -p program - cargo build -p program --target riscv32im-unknown-none-elf - @echo "=== Program build complete ===" - examples: @echo "=== Building example programs ===" $(MAKE) -C crates/examples @@ -32,7 +43,6 @@ examples: test: generate_abis @echo "=== Running tests ===" cargo test -p types -p storage -p state -- --nocapture - cargo test -p program -- --nocapture cargo test -p vm -- --nocapture cargo test -p compiler -- --nocapture cd crates/examples && cargo test -- --nocapture @@ -54,7 +64,6 @@ summary: @echo "πŸŽ‰ BUILD SUMMARY" @echo "================" @echo "βœ… Cleaned project artifacts" - @echo "βœ… Built program crate for RISC-V target" @echo "βœ… Built example programs:" @echo " - allocator_demo: Memory allocation demonstration" @echo " - call_program: Cross-contract call demonstration" @@ -72,7 +81,6 @@ summary: @echo " - types" @echo " - storage" @echo " - state" - @echo " - program" @echo " - vm" @echo " - compiler" @echo "βœ… Tested VM instruction soundness:" diff --git a/README.md b/README.md index a6a8bca..e49b321 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ Blockchain technology is revolutionizing how we think about trust, decentralizat ``` rust-vm/ β”œβ”€β”€ crates/ -β”‚ β”œβ”€β”€ avm/ # Application Virtual Machine - main orchestrator +β”‚ β”œβ”€β”€ bootloader/ # Guest bootloader and test harness integration +β”‚ β”œβ”€β”€ kernel/ # Guest kernel (tasks, syscalls, memory layout) β”‚ β”œβ”€β”€ compiler/ # Rust-to-bytecode compiler β”‚ β”œβ”€β”€ examples/ # Smart contract examples and tutorials β”‚ β”‚ β”œβ”€β”€ README.md # πŸ“– [Detailed guide to all examples](crates/examples/README.md) @@ -116,9 +117,10 @@ rust-vm/ β”‚ β”‚ β”œβ”€β”€ storage.rs # Storage system examples β”‚ β”‚ β”œβ”€β”€ multi_func.rs # Multi-function contract β”‚ β”‚ └── call_program.rs # Cross-program calls -β”‚ β”œβ”€β”€ program/ # Smart contract runtime library +β”‚ β”œβ”€β”€ clibc/ # Chain Libc smart contract runtime library β”‚ β”œβ”€β”€ state/ # Blockchain state management β”‚ β”œβ”€β”€ storage/ # Persistent storage system +β”‚ β”œβ”€β”€ test-suite/ # End-to-end and integration tests β”‚ β”œβ”€β”€ types/ # Common types and data structures β”‚ └── vm/ # RISC-V virtual machine core β”‚ └── tests/ # RISC-V compliance tests @@ -131,22 +133,18 @@ rust-vm/ ### πŸ›οΈ **Architecture Overview** ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Transaction β”‚ β”‚ Smart Contractβ”‚ β”‚ RISC-V VM β”‚ -β”‚ Processing │───▢│ Execution │───▢│ Core β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ State Mgmt β”‚ β”‚ Memory Mgmt β”‚ β”‚ Instruction β”‚ -β”‚ (Accounts) β”‚ β”‚ (Pages) β”‚ β”‚ Decoder β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Storage β”‚ β”‚ Context Stack β”‚ β”‚ CPU & Regs β”‚ -β”‚ (Persistent) β”‚ β”‚ (Call Chain) β”‚ β”‚ (Execution) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Bootloader │─▢│ Kernel │─▢│ User Tasks β”‚ +β”‚ (BootInfo, β”‚ β”‚ init, traps, β”‚ β”‚ (addr space, β”‚ +β”‚ VM setup) β”‚ β”‚ syscalls) β”‚ β”‚ trapframe) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + v v + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ State + Storage β”‚ β”‚ RISC-V VM β”‚ + β”‚ (accounts, β”‚ β”‚ (CPU + decoder) β”‚ + β”‚ receipts) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- diff --git a/aTester/Cargo.toml b/aTester/Cargo.toml new file mode 100644 index 0000000..19bdc3b --- /dev/null +++ b/aTester/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "a_tests" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" + +[dependencies] +compiler = { path = "../crates/compiler" } +goblin = "0.8" +types = { path = "../crates/types" } +vm = { path = "../crates/vm" } + +[dev-dependencies] +state = { path = "../crates/state" } diff --git a/aTester/README.md b/aTester/README.md new file mode 100644 index 0000000..7107101 --- /dev/null +++ b/aTester/README.md @@ -0,0 +1,13 @@ +# aTests + +Architecture-aware test suite scaffold. + +Intent: +- Run ELF binaries on a supplied architecture runner (VM, QEMU, etc). +- Allow multiple test kinds with different assertion logic. +- Keep the runner abstract and pluggable. + +Current shape: +- `ArchRunner`: runs an ELF on an architecture and returns logs/exit code. +- `TestEvaluator`: evaluates a `RunResult` based on `TestCase` kind. +- `Suite`: runs a list of test cases through a runner. diff --git a/aTester/src/arch.rs b/aTester/src/arch.rs new file mode 100644 index 0000000..1a73ac3 --- /dev/null +++ b/aTester/src/arch.rs @@ -0,0 +1,50 @@ +use std::fmt; + +use crate::types::{ElfTarget, RunOptions}; + +#[derive(Debug, Clone)] +pub struct RunResult { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, + pub output: Vec, +} + +#[derive(Debug)] +pub struct RunError { + pub message: String, +} + +impl fmt::Display for RunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for RunError {} + +pub trait ArchRunner { + fn name(&self) -> &str; + fn run(&self, elf: &ElfTarget, options: &RunOptions) -> Result; +} + +pub struct ArchRegistry { + runners: Vec>, +} + +impl ArchRegistry { + pub fn new() -> Self { + Self { runners: Vec::new() } + } + + pub fn register(&mut self, runner: Box) { + self.runners.push(runner); + } + + pub fn get(&self, name: &str) -> Option<&dyn ArchRunner> { + self.runners + .iter() + .find(|runner| runner.name() == name) + .map(|runner| runner.as_ref()) + } +} diff --git a/aTester/src/lib.rs b/aTester/src/lib.rs new file mode 100644 index 0000000..cb7db94 --- /dev/null +++ b/aTester/src/lib.rs @@ -0,0 +1,9 @@ +mod arch; +mod runners; +mod suite; +mod types; + +pub use arch::{ArchRegistry, ArchRunner, RunError, RunResult}; +pub use runners::AvmRunner; +pub use suite::{Suite, TestCase, TestKind, TestReport, TestEvaluator}; +pub use types::{ElfTarget, RunOptions, TestOutcome}; diff --git a/aTester/src/runners/avm.rs b/aTester/src/runners/avm.rs new file mode 100644 index 0000000..81036af --- /dev/null +++ b/aTester/src/runners/avm.rs @@ -0,0 +1,275 @@ +use std::cell::{Cell, RefCell}; +use std::fmt::Write as FmtWrite; +use std::fs; +use std::mem; +use std::rc::Rc; + +use compiler::elf::parse_elf_from_bytes; +use goblin::elf::Elf; +use types::boot::BootInfo; +use types::kernel_result::KERNEL_RESULT_ADDR; +use types::SV32_DIRECT_MAP_BASE; +use vm::memory::{API, MMU, Perms, Sv32Memory, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SIZE}; +use vm::registers::Register; +use vm::vm::VM; + +use crate::arch::{ArchRunner, RunError, RunResult}; +use crate::types::{ElfTarget, RunOptions}; + +pub struct AvmRunner; + +impl AvmRunner { + pub fn new() -> Self { + Self + } +} + +impl ArchRunner for AvmRunner { + fn name(&self) -> &str { + "avm" + } + + fn run(&self, elf: &ElfTarget, options: &RunOptions) -> Result { + let elf_bytes = fs::read(&elf.path).map_err(|e| RunError { + message: format!("failed to read elf {}: {e}", elf.path.display()), + })?; + + let total_size = options.vm_memory_size.unwrap_or(16 * 1024 * 1024); + let memory = Rc::new(Sv32Memory::new(total_size, PAGE_SIZE)); + let heap_ptr = Rc::new(Cell::new(0u32)); + let entry_point = load_kernel(&elf_bytes, &memory, heap_ptr.as_ref())?; + + if options.input.len() > 3usize { + return Err(RunError { + message: format!("too many inputs ({}); max is 3", options.input.len()), + }); + } + let mut input_ptrs = [0u32; 3]; + let mut input_lens = [0u32; 3]; + for idx in 0..options.input.len() { + let bytes = options + .input + .get(idx) + .map(|input| input.as_slice()) + .unwrap_or(&[]); + let ptr = alloc_on_heap(memory.as_ref(), heap_ptr.as_ref(), bytes); + input_ptrs[idx] = ptr; + input_lens[idx] = bytes.len() as u32; + } + let boot_info_ptr = place_boot_info(memory.as_ref(), heap_ptr.as_ref(), total_size)?; + + let mut vm = VM::new(memory.clone()); + vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); + vm.cpu.verbose = options.verbose; + + let writer = Rc::new(RefCell::new(StringWriter::default())); + vm.cpu.set_verbose_writer(writer.clone()); + vm.cpu.pc = entry_point; + + // set input regs + const ARG_REGS: [Register; 8] = [ + Register::A0, + Register::A1, + Register::A2, + Register::A3, + Register::A4, + Register::A5, + Register::A6, + Register::A7, + ]; + for (idx, ptr) in input_ptrs.iter().enumerate() { + let reg_idx = idx * 2; + vm.set_reg_u32(ARG_REGS[reg_idx], *ptr); + vm.set_reg_u32(ARG_REGS[reg_idx + 1], input_lens[idx]); + } + let boot_reg_idx = options.input.len() * 2; + if boot_reg_idx >= ARG_REGS.len() { + return Err(RunError { + message: "no argument register available for boot info".to_string(), + }); + } + vm.set_reg_u32(ARG_REGS[boot_reg_idx], boot_info_ptr); + if boot_reg_idx + 1 < ARG_REGS.len() { + vm.set_reg_u32(ARG_REGS[boot_reg_idx + 1], 0); + } + + vm.raw_run(); + + let stdout = writer.borrow().buffer.clone(); + let output = read_kernel_blob(memory.as_ref()).unwrap_or_default(); + let exit_code = 0; + let stderr = String::new(); + + Ok(RunResult { + exit_code, + stdout, + stderr, + output, + }) + } +} + +const KERNEL_WINDOW_BYTES: usize = 4 * 1024 * 1024; +const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; +const KERNEL_RESULT_DUMP_BYTES: u32 = 1024 * 1024; + +fn load_kernel( + elf_bytes: &[u8], + memory: &Rc, + heap_ptr: &Cell, +) -> Result { + let elf = parse_elf_from_bytes(elf_bytes).map_err(|e| RunError { + message: format!("failed to parse kernel elf: {e}"), + })?; + let entry_point = Elf::parse(elf_bytes) + .map_err(|e| RunError { + message: format!("failed to parse entry point: {e}"), + })? + .entry as u32; + + let (code, code_base) = elf + .get_flat_code() + .ok_or_else(|| RunError { + message: "kernel elf missing .text".to_string(), + })?; + let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); + let (bss, bss_base) = elf.get_flat_bss().unwrap_or((Vec::new(), code_base)); + + let mut min_base = core::cmp::min(code_base, ro_base) as usize; + if !bss.is_empty() { + min_base = core::cmp::min(min_base, bss_base as usize); + } + let code_end = (code_base + code.len() as u64) as usize; + let ro_end = (ro_base + rodata.len() as u64) as usize; + let mut image_end = core::cmp::max(code_end, ro_end); + if !bss.is_empty() { + let bss_end = bss_base + .checked_add(bss.len() as u64) + .ok_or_else(|| RunError { + message: "bss end overflow".to_string(), + })? as usize; + image_end = core::cmp::max(image_end, bss_end); + } + let image_size = image_end + .checked_sub(min_base) + .ok_or_else(|| RunError { + message: "invalid image size".to_string(), + })?; + + if image_end > memory.size() { + return Err(RunError { + message: format!( + "elf image does not fit in mapped memory (need {}, have {})", + image_end, + memory.size() + ), + }); + } + if KERNEL_WINDOW_BYTES > memory.size() { + return Err(RunError { + message: format!( + "kernel window exceeds physical memory (need {}, have {})", + KERNEL_WINDOW_BYTES, + memory.size() + ), + }); + } + + let mut image = vec![0u8; image_size]; + let code_off = (code_base as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + let ro_off = (ro_base as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); + } + if !bss.is_empty() { + let bss_off = (bss_base as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } + + memory.map_range(VirtualAddress(0), KERNEL_WINDOW_BYTES, Perms::rwx_kernel()); + memory.write_bytes(VirtualAddress(min_base as u32), &image); + + let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; + heap_ptr.set(heap_start); + + let mapped = memory.map_physical_range( + VirtualAddress(SV32_DIRECT_MAP_BASE), + 0, + memory.size(), + Perms::rw_kernel(), + ); + if !mapped { + return Err(RunError { + message: "failed to map kernel direct physical window".to_string(), + }); + } + + Ok(entry_point) +} + +fn read_kernel_blob(memory: &Sv32Memory) -> Option> { + let start = VirtualAddress(KERNEL_RESULT_ADDR); + let end = start.checked_add(KERNEL_RESULT_DUMP_BYTES)?; + let slice = memory.mem_slice(start, end)?; + Some(slice.as_ref().to_vec()) +} + +fn place_boot_info(memory: &Sv32Memory, heap_ptr: &Cell, memory_size: usize) -> Result { + let heap_start = ensure_heap_ptr(heap_ptr); + let aligned_heap = (heap_start + 7) & !7; + let boot_info_size = mem::size_of::() as u32; + let next_heap = aligned_heap + .checked_add(boot_info_size) + .and_then(|v| v.checked_add(HEAP_PTR_OFFSET)) + .ok_or_else(|| RunError { + message: "boot info heap pointer overflow".to_string(), + })?; + let boot_info = BootInfo::new( + memory.current_root() as u32, + KERNEL_STACK_TOP, + next_heap, + memory_size as u32, + memory.next_free_ppn() as u32, + 0, + KERNEL_WINDOW_BYTES as u32, + ); + let bytes = unsafe { + core::slice::from_raw_parts( + &boot_info as *const BootInfo as *const u8, + mem::size_of::(), + ) + }; + let addr = alloc_on_heap(memory, heap_ptr, bytes); + Ok(addr) +} + +fn alloc_on_heap(memory: &Sv32Memory, heap_ptr: &Cell, data: &[u8]) -> u32 { + let addr = ensure_heap_ptr(heap_ptr); + memory.write_bytes(VirtualAddress(addr), data); + let next = (addr + data.len() as u32 + HEAP_PTR_OFFSET + 7) & !7; + heap_ptr.set(next); + addr +} + +fn ensure_heap_ptr(heap_ptr: &Cell) -> u32 { + let current = heap_ptr.get(); + if current == 0 { + heap_ptr.set(HEAP_PTR_OFFSET); + HEAP_PTR_OFFSET + } else { + current + } +} + +#[derive(Default)] +struct StringWriter { + buffer: String, +} + +impl FmtWrite for StringWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.buffer.push_str(s); + Ok(()) + } +} diff --git a/aTester/src/runners/mod.rs b/aTester/src/runners/mod.rs new file mode 100644 index 0000000..905f9c5 --- /dev/null +++ b/aTester/src/runners/mod.rs @@ -0,0 +1,3 @@ +mod avm; + +pub use avm::AvmRunner; diff --git a/aTester/src/suite.rs b/aTester/src/suite.rs new file mode 100644 index 0000000..60fc6bd --- /dev/null +++ b/aTester/src/suite.rs @@ -0,0 +1,66 @@ +use std::path::PathBuf; + +use crate::arch::{ArchRunner, RunResult}; +use crate::types::{ElfTarget, RunOptions, TestOutcome}; + +#[derive(Debug, Clone)] +pub enum TestKind { + Smoke, + OutputMatch, + InstructionTrace, +} + +#[derive(Debug, Clone)] +pub struct TestCase { + pub name: String, + pub kind: TestKind, + pub elf: PathBuf, + pub options: RunOptions, +} + +#[derive(Debug, Clone)] +pub struct TestReport { + pub name: String, + pub outcome: TestOutcome, + pub runner: String, + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + +pub trait TestEvaluator { + fn evaluate(&self, case: &TestCase, result: &RunResult) -> TestOutcome; +} + +pub struct Suite<'a> { + pub name: String, + pub cases: Vec, + pub evaluator: &'a dyn TestEvaluator, +} + +impl<'a> Suite<'a> { + pub fn run(&self, runner: &dyn ArchRunner) -> Vec { + let mut reports = Vec::new(); + for case in &self.cases { + let elf = ElfTarget { + path: case.elf.clone(), + }; + let (outcome, exit_code, stdout, stderr) = match runner.run(&elf, &case.options) { + Ok(result) => { + let outcome = self.evaluator.evaluate(case, &result); + (outcome, result.exit_code, result.stdout, result.stderr) + } + Err(err) => (TestOutcome::Failed(err.message.clone()), -1, String::new(), err.message), + }; + reports.push(TestReport { + name: case.name.clone(), + outcome, + runner: runner.name().to_string(), + exit_code, + stdout, + stderr, + }); + } + reports + } +} diff --git a/aTester/src/types.rs b/aTester/src/types.rs new file mode 100644 index 0000000..215e3f5 --- /dev/null +++ b/aTester/src/types.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct ElfTarget { + pub path: PathBuf, +} + +#[derive(Debug, Clone, Default)] +pub struct RunOptions { + pub timeout_ms: Option, + pub vm_memory_size: Option, + pub verbose: bool, + pub input: Vec>, +} + +#[derive(Debug, Clone)] +pub enum TestOutcome { + Passed, + Failed(String), + Skipped(String), +} diff --git a/aTester/tests/examples.rs b/aTester/tests/examples.rs new file mode 100644 index 0000000..9a610cf --- /dev/null +++ b/aTester/tests/examples.rs @@ -0,0 +1,180 @@ +use std::path::{Path, PathBuf}; + +use a_tests::{AvmRunner, RunOptions, Suite, TestCase, TestEvaluator, TestKind, TestOutcome}; +use types::TransactionReceipt; + +#[path = "fixtures/examples.rs"] +mod fixtures; + +use fixtures::{all_example_cases, expected_for, test_state_bytes}; + +struct ExampleEvaluator; + +impl TestEvaluator for ExampleEvaluator { + fn evaluate(&self, case: &TestCase, result: &a_tests::RunResult) -> TestOutcome { + let receipts_slice = match kernel_receipts_slice(&result.output) { + Some(slice) => slice, + None => return TestOutcome::Failed("kernel receipts not in dump".to_string()), + }; + let receipts = match TransactionReceipt::decode_list(receipts_slice) { + Some(receipts) => receipts, + None => return TestOutcome::Failed("failed to decode receipts".to_string()), + }; + let receipt = match receipts.last() { + Some(receipt) => receipt, + None => return TestOutcome::Failed("missing transaction receipt".to_string()), + }; + let expected = match expected_for(case.name.as_str()) { + Some(expected) => expected, + None => { + return TestOutcome::Failed(format!( + "missing expected result for {}", + case.name + )) + } + }; + let success = receipt.result.success; + let error_code = receipt.result.error_code; + let data_len = receipt.result.data_len; + let data = receipt.result.data; + if success != expected.success { + return TestOutcome::Failed(format!( + "expected success={}, got {}", + expected.success, success + )); + } + if error_code != expected.error_code { + return TestOutcome::Failed(format!( + "expected error_code={}, got {}", + expected.error_code, error_code + )); + } + let data_len = data_len as usize; + let actual = &data[..data_len.min(data.len())]; + if actual != expected.data.as_slice() { + return TestOutcome::Failed(format!( + "expected data {:?}, got {:?}", + expected.data, actual + )); + } + TestOutcome::Passed + } +} + +#[test] +fn examples_tests() { + build_kernel().expect("failed to build kernel"); + build_examples().expect("failed to build example programs"); + + let target_dir = kernel_elf_dir(); + let state_bytes = test_state_bytes(); + let cases = all_example_cases() + .expect("failed to build example bundles") + .into_iter() + .map(|case| TestCase { + name: case.name.to_string(), + kind: TestKind::Smoke, + elf: target_dir.join("kernel.elf"), + options: RunOptions { + timeout_ms: None, + vm_memory_size: None, + verbose: false, + input: vec![case.bundle.encode(), state_bytes.clone()], + }, + }) + .collect::>(); + + let evaluator = ExampleEvaluator; + let suite = Suite { + name: "examples_tests".to_string(), + cases, + evaluator: &evaluator, + }; + + let runner = AvmRunner::new(); + let reports = suite.run(&runner); + + for report in &reports { + if !report.stdout.is_empty() { + println!("--- {} stdout ---\n{}", report.name, report.stdout); + } + if !report.stderr.is_empty() { + eprintln!("--- {} stderr ---\n{}", report.name, report.stderr); + } + } + + let failures: Vec<_> = reports + .iter() + .filter(|report| matches!(report.outcome, TestOutcome::Failed(_))) + .collect(); + + if !failures.is_empty() { + let mut details = String::new(); + for report in failures { + if let TestOutcome::Failed(detail) = &report.outcome { + details.push_str(&format!("{}: {}\n", report.name, detail)); + } + } + panic!("example test failures:\n{}", details); + } +} + +fn kernel_elf_dir() -> PathBuf { + std::env::var("KERNEL_ELF_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root().join("crates/bootloader/bin")) +} + +fn build_kernel() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["kernel"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn kernel make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("kernel build failed with status: {status}")) + } +} + +fn build_examples() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["-C", "crates/examples"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn examples make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("examples build failed with status: {status}")) + } +} + +fn workspace_root() -> PathBuf { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(PathBuf::from) + .expect("missing workspace root") +} + +fn kernel_receipts_slice(dump: &[u8]) -> Option<&[u8]> { + if dump.len() < 16 { + return None; + } + let receipts_ptr = u32::from_le_bytes(dump[0..4].try_into().ok()?); + let receipts_len = u32::from_le_bytes(dump[4..8].try_into().ok()?); + if receipts_ptr == 0 || receipts_len == 0 { + return None; + } + let base = types::kernel_result::KERNEL_RESULT_ADDR; + let start = receipts_ptr.checked_sub(base)? as usize; + let end = start.checked_add(receipts_len as usize)?; + if end > dump.len() { + return None; + } + Some(&dump[start..end]) +} diff --git a/aTester/tests/fixtures/examples.rs b/aTester/tests/fixtures/examples.rs new file mode 100644 index 0000000..e295fad --- /dev/null +++ b/aTester/tests/fixtures/examples.rs @@ -0,0 +1,612 @@ +use compiler::elf::parse_elf_from_bytes; +use state; +use types::address::Address; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; + +pub struct ExpectedResult { + pub success: bool, + pub error_code: u32, + pub data: Vec, +} + +pub struct ExampleCase { + pub name: &'static str, + pub description: &'static str, + pub bundle: TransactionBundle, +} + +pub fn test_state_bytes() -> Vec { + let mut state = state::State::new(); + for addr_hex in [ + "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2", + "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3", + ] { + let addr = to_address(addr_hex); + let account = state.get_account_mut(&addr); + account.balance = 1_000_000_000u128; + } + state.encode() +} + +pub fn all_example_cases() -> Result, String> { + Ok(vec![ + ExampleCase { + name: "erc20", + description: "ERC-20 init, transfer, and balance query flow", + bundle: build_erc20_bundle()?, + }, + ExampleCase { + name: "call program", + description: "Cross-contract call with nested program execution", + bundle: build_call_program_bundle()?, + }, + ExampleCase { + name: "account create (storage)", + description: "Create a contract and invoke a storage call", + bundle: build_account_create_storage_bundle()?, + }, + ExampleCase { + name: "account create (simple)", + description: "Create a simple contract and verify return data", + bundle: build_account_create_simple_bundle()?, + }, + ExampleCase { + name: "multi function (simple)", + description: "Router-style call into a multi-function contract", + bundle: build_multi_function_simple_bundle()?, + }, + ExampleCase { + name: "allocator demo", + description: "Heap allocation and collection usage in guest code", + bundle: build_allocator_demo_bundle()?, + }, + ExampleCase { + name: "native transfer", + description: "Native value transfer without a contract call", + bundle: build_native_transfer_bundle(), + }, + ExampleCase { + name: "guest transfer syscall", + description: "Program issues a native transfer syscall", + bundle: build_guest_transfer_syscall_bundle()?, + }, + ExampleCase { + name: "dex amm", + description: "AMM lifecycle: init, approve, add/remove liquidity, swap", + bundle: build_dex_amm_bundle()?, + }, + ExampleCase { + name: "ecdsa verify", + description: "ECDSA signature verification within the VM", + bundle: build_ecdsa_verify_bundle()?, + }, + ]) +} + +pub fn expected_for(name: &str) -> Option { + match name { + "erc20" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![128, 240, 250, 2], + }), + "call program" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "account create (storage)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "account create (simple)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "multi function (simple)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "allocator demo" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "native transfer" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "guest transfer syscall" => Some(ExpectedResult { + success: true, + error_code: 0, + data: 42u128.to_le_bytes().to_vec(), + }), + "dex amm" => { + let mut buf = Vec::new(); + buf.extend_from_slice(&101000u128.to_le_bytes()); + buf.extend_from_slice(&495050u128.to_le_bytes()); + Some(ExpectedResult { + success: true, + error_code: 0, + data: buf, + }) + } + "ecdsa verify" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + _ => None, + } +} + +struct HostFuncCall { + selector: u8, + args: Vec, +} + +fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { + let mut encoded = Vec::new(); + for call in calls { + let len = call.args.len(); + assert!(len <= 255, "argument too long for 1-byte length field"); + encoded.push(call.selector); + encoded.push(len as u8); + encoded.extend_from_slice(&call.args); + } + encoded +} + +fn build_erc20_bundle() -> Result { + let deployer = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + let contract = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + from: deployer, + to: contract, + data: get_program_code("erc20")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: (|| { + let max_supply: u32 = 100000000; + let mut max_supply_bytes = max_supply.to_le_bytes().to_vec(); + max_supply_bytes.extend(vec![18u8]); + max_supply_bytes + })(), + }]), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x02, + args: (|| { + let to_addr = + to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); + let mut args = to_addr.0.to_vec(); + let amount: u32 = 50000000; + args.extend(amount.to_le_bytes()); + args + })(), + }]), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x05, + args: (|| { + let owner = + to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + owner.0.to_vec() + })(), + }]), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_call_program_bundle() -> Result { + let caller = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + let callee = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + from: caller, + to: caller, + data: get_program_code("call_program")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::CreateAccount, + from: caller, + to: callee, + data: get_program_code("simple")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: caller, + from: caller, + data: (|| { + let mut data = callee.0.to_vec(); + data.extend(vec![100, 0, 0, 0, 42, 0, 0, 0]); + data + })(), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_account_create_storage_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("storage")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_account_create_simple_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("simple")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![100, 0, 0, 0, 42, 0, 0, 0], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_multi_function_simple_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("multi_func")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: vec![100, 0, 0, 0, 42, 0, 0, 0], + }]), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_allocator_demo_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("allocator_demo")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![ + 12, 0, 0, 0, 15, 0, 0, 0, 100, 0, 0, 0, 95, 0, 0, 0, 87, 0, 0, 0, 92, 0, + 0, 0, + ], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_native_transfer_bundle() -> TransactionBundle { + TransactionBundle::new(vec![Transaction { + tx_type: TransactionType::Transfer, + to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), + from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), + data: vec![], + value: 10, + nonce: 0, + }]) +} + +fn build_guest_transfer_syscall_bundle() -> Result { + let program = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"); + let sender = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"); + let recipient = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: program, + from: sender, + data: get_program_code("native_transfer")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: program, + from: sender, + data: (|| { + let mut data = recipient.0.to_vec(); + data.extend_from_slice(&42u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 1, + }, + ])) +} + +fn build_dex_amm_bundle() -> Result { + let erc20 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + let dex = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"); + let user2 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); + let user3 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: erc20, + from: user3, + data: get_program_code("erc20")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: erc20, + from: user3, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: (|| { + let mut args = Vec::new(); + let supply: u32 = 1_000_000; + args.extend_from_slice(&supply.to_le_bytes()); + args.push(0); + args + })(), + }]), + value: 0, + nonce: 1, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: erc20, + from: user3, + data: encode_router_calls(&[HostFuncCall { + selector: 0x03, + args: (|| { + let mut args = dex.0.to_vec(); + let amount: u32 = 500_000; + args.extend_from_slice(&amount.to_le_bytes()); + args + })(), + }]), + value: 0, + nonce: 2, + }, + Transaction { + tx_type: TransactionType::CreateAccount, + to: dex, + from: user3, + data: get_program_code("dex")?, + value: 0, + nonce: 3, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user3, + data: (|| { + let mut data = Vec::new(); + data.push(0x01); + data.extend_from_slice(&100_000u64.to_le_bytes()); + data.extend_from_slice(&500_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 4, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user2, + data: (|| { + let mut data = Vec::new(); + data.push(0x03); + data.push(0x00); + data.extend_from_slice(&1_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user3, + data: (|| { + let mut data = Vec::new(); + data.push(0x02); + data.extend_from_slice(&100_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 5, + }, + ])) +} + +fn build_ecdsa_verify_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("ecdsa_verify")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: build_ecdsa_payload(), + value: 0, + nonce: 1, + }, + ])) +} + +fn build_ecdsa_payload() -> Vec { + let mut payload = Vec::with_capacity(1 + ECDSA_PK_BYTES.len() + ECDSA_SIG_BYTES.len() + ECDSA_HASH.len()); + payload.push(ECDSA_PK_BYTES.len() as u8); + payload.extend_from_slice(&ECDSA_PK_BYTES); + payload.extend_from_slice(&ECDSA_SIG_BYTES); + payload.extend_from_slice(&ECDSA_HASH); + payload +} + +const ECDSA_HASH: [u8; 32] = [ + 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, + 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, +]; +const ECDSA_PK_BYTES: [u8; 33] = [ + 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, + 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, + 0x0c, +]; +const ECDSA_SIG_BYTES: [u8; 64] = [ + 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, + 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, + 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, + 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, +]; + +fn get_program_code(name: &str) -> Result, String> { + let bytes = read_example_bin(name)?; + let elf = parse_elf_from_bytes(&bytes) + .map_err(|e| format!("failed to parse elf for {name}: {e}"))?; + + let (code, code_start) = elf + .get_flat_code() + .ok_or_else(|| format!("no code section for {name}"))?; + let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((Vec::new(), u64::MAX)); + + let mut total_len = code_start + code.len() as u64; + if !rodata.is_empty() { + total_len = rodata_start + rodata.len() as u64; + } + + let mut combined = vec![0u8; total_len as usize]; + combined[code_start as usize..code_start as usize + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + combined[rodata_start as usize..rodata_start as usize + rodata.len()] + .copy_from_slice(&rodata); + } + Ok(combined) +} + +fn read_example_bin(name: &str) -> Result, String> { + let base = workspace_root().join("crates/examples/bin"); + let mut candidates = vec![base.join(name), base.join(format!("{name}.elf"))]; + candidates.push( + workspace_root() + .join("target/avm32/release") + .join(name), + ); + + for path in candidates { + if path.exists() { + return std::fs::read(&path) + .map_err(|e| format!("failed to read {}: {e}", path.display())); + } + } + Err(format!( + "missing example binary for {name}; try running make -C crates/examples" + )) +} + +fn to_address(hex: &str) -> Address { + assert!(hex.len() == 40, "hex string must be 40 characters"); + fn from_hex_char(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("invalid hex character"), + } + } + let mut bytes = [0u8; 20]; + let hex_bytes = hex.as_bytes(); + for i in 0..20 { + let hi = from_hex_char(hex_bytes[i * 2]); + let lo = from_hex_char(hex_bytes[i * 2 + 1]); + bytes[i] = (hi << 4) | lo; + } + Address(bytes) +} + +fn workspace_root() -> std::path::PathBuf { + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(std::path::PathBuf::from) + .expect("missing workspace root") +} diff --git a/aTester/tests/kernel.rs b/aTester/tests/kernel.rs new file mode 100644 index 0000000..f289fd9 --- /dev/null +++ b/aTester/tests/kernel.rs @@ -0,0 +1,165 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use a_tests::{AvmRunner, RunOptions, Suite, TestCase, TestEvaluator, TestKind, TestOutcome}; + +struct ExitCodeEvaluator; + +impl TestEvaluator for ExitCodeEvaluator { + fn evaluate(&self, case: &TestCase, result: &a_tests::RunResult) -> TestOutcome { + match read_test_results_from_output(&result.output) { + Ok(results) => { + if results.status == 0 { + TestOutcome::Passed + } else { + TestOutcome::Failed(format!( + "{} failed with detail {}", + case.name, results.detail + )) + } + } + Err(err) => TestOutcome::Failed(format!("{} failed: {}", case.name, err)), + } + } +} + +#[test] +fn kernel_tests() { + build_kernel().expect("failed to build kernel test bins"); + let bins = kernel_bins().expect("failed to discover kernel bins"); + if bins.is_empty() { + panic!("no kernel test bins found"); + } + + let target_dir = kernel_elf_dir(); + let cases = bins + .into_iter() + .map(|name| TestCase { + name: name.clone(), + kind: TestKind::Smoke, + elf: target_dir.join(format!("{name}.elf")), + options: RunOptions { + timeout_ms: None, + vm_memory_size: None, + verbose: false, + input: Vec::new(), + }, + }) + .collect::>(); + + let evaluator = ExitCodeEvaluator; + let suite = Suite { + name: "kernel_tests".to_string(), + cases, + evaluator: &evaluator, + }; + + let runner = AvmRunner::new(); + for case in &suite.cases { + println!("running kernel test: {}", case.name); + } + let reports = suite.run(&runner); + + for report in &reports { + if !report.stdout.is_empty() { + println!("--- {} stdout ---\n{}", report.name, report.stdout); + } + if !report.stderr.is_empty() { + eprintln!("--- {} stderr ---\n{}", report.name, report.stderr); + } + } + + let failures: Vec<_> = reports + .iter() + .filter(|report| matches!(report.outcome, TestOutcome::Failed(_))) + .collect(); + + if !failures.is_empty() { + let mut details = String::new(); + for report in failures { + if let TestOutcome::Failed(detail) = &report.outcome { + details.push_str(&format!("{}: {}\n", report.name, detail)); + } + } + panic!("kernel test failures:\n{}", details); + } +} + +struct TestResults { + status: u32, + detail: u32, +} + +fn read_test_results_from_output(output: &[u8]) -> Result { + if output.len() < 8 { + return Err("missing test results output".to_string()); + } + let status = u32::from_le_bytes(output[0..4].try_into().unwrap()); + let detail = u32::from_le_bytes(output[4..8].try_into().unwrap()); + Ok(TestResults { status, detail }) +} + +fn kernel_bins() -> Result, String> { + let manifest_path = workspace_root().join("crates/kernel/Cargo.toml"); + let contents = fs::read_to_string(&manifest_path) + .map_err(|e| format!("failed to read kernel Cargo.toml: {e}"))?; + + let mut bins = Vec::new(); + let mut current_name: Option = None; + + for line in contents.lines() { + let line = line.trim(); + if line == "[[bin]]" { + if let Some(name) = current_name.take() { + if name != "kernel" { + bins.push(name); + } + } + continue; + } + if let Some(name) = line.strip_prefix("name = ") { + let name = name.trim().trim_matches('"').to_string(); + current_name = Some(name); + } + } + + if let Some(name) = current_name { + if name != "kernel" { + bins.push(name); + } + } + + if bins.is_empty() { + return Err("no [[bin]] entries found".to_string()); + } + + Ok(bins) +} + +fn kernel_elf_dir() -> PathBuf { + std::env::var("KERNEL_ELF_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root().join("crates/bootloader/bin")) +} + +fn build_kernel() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["kernel"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn kernel make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("kernel build failed with status: {status}")) + } +} + +fn workspace_root() -> PathBuf { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(PathBuf::from) + .expect("missing workspace root") +} diff --git a/crates/avm/Cargo.toml b/crates/avm/Cargo.toml deleted file mode 100644 index 6e197be..0000000 --- a/crates/avm/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "avm" -version = "0.1.0" -edition = "2024" - -[dependencies] -hex = { version = "0.4", default-features = false } -compiler = { path = "../compiler" } # adjust path as needed -state = { path = "../state" } # adjust path as needed -storage = { path = "../storage" } # adjust path as needed -vm = { path = "../vm" } # adjust path as needed -types = { path = "../types" } # adjust path as needed \ No newline at end of file diff --git a/crates/avm/README.md b/crates/avm/README.md deleted file mode 100644 index 942bca2..0000000 --- a/crates/avm/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# AVM - Alon's Virtual Machine - -AVM is a custom virtual machine designed to execute Rust-compiled contracts in a sandboxed, stack-based environment. It features an isolated execution model where each contract call is executed within its own independent VM context. - ---- - -## 🧠 Core Concepts - -### Execution Stack - -AVM maintains an **execution stack** that tracks active `ExecutionContext` frames. Each time a contract calls another contract, a **new context** is pushed onto the stack. - -- The **top of the stack** is the currently executing context. -- When a call finishes, the context is popped and control returns to the caller. -- This model ensures **synchronous execution** and supports **reentrancy** and **deep contract composition**. - -### ExecutionContext - -An `ExecutionContext` is a complete, self-contained VM instance with its own: - -- **Registers** (`[u64; 32]`) -- **Program Counter (`PC`)** -- **Memory Page** (linear memory, typically fixed-size per context) -- **Stack Pointer / Heap management** -- **Gas metering** (optional but recommended) -- **Access to the syscall interface** - -Each context behaves like a separate "machine" running in isolation. - ---- - -## πŸ”„ Contract-to-Contract Calls - -When Contract A calls Contract B: -1. A new `ExecutionContext` is initialized with: - - B's bytecode loaded - - Fresh memory and registers - - Arguments passed via memory or registers -2. The new context is **pushed** onto the execution stack. -3. Execution begins in the new context. -4. Upon return, the result is passed back to the caller, and the callee context is **popped**. - -This mechanism provides: -- Full isolation between contracts -- Clean memory separation (no shared heap or stack) -- Easy error handling (unwinding stack on `panic`) - ---- - -## πŸ“¦ Memory Model - -Each context is allocated its own **linear memory page**, which includes: - -- `.text` (program code, optional if interpreted) -- `.rodata` and statics (copied at load time) -- Stack (grows down from high memory) -- Heap (grows up from a defined base) - -All memory access is **local to the context**, preventing accidental overwrites between contracts. - ---- - -## πŸš€ Features (Planned or In Progress) - -- [x] RISC-V instruction decoding (32-bit and compressed) -- [x] Memory-mapped syscall interface -- [x] Per-context execution model -- [ ] Gas accounting and metering -- [ ] Persistent storage via key-based syscalls -- [ ] Support for `vm_panic` and return codes -- [ ] Debug output and tracing - ---- - -## πŸ§ͺ Example Use Case - -```rust -#[contract] -fn contract_a() { - call_contract("contract_b", &[arg1, arg2]); -} diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs deleted file mode 100644 index 8e4ced2..0000000 --- a/crates/avm/src/avm.rs +++ /dev/null @@ -1,462 +0,0 @@ -use crate::memory_page_manager::MemoryPageManager; -use crate::receipt::TransactionReceipt; -use storage::Storage; -use vm::vm::VM; -use vm::registers::Register; -use state::{State, Account}; -use crate::transaction::{TransactionType, Transaction}; -use crate::global::Config; -use crate::execution_context::{ExecutionContext, ContextStack}; -use crate::host_interface::HostShim; -use types::address::Address; -use types::result::Result; -use std::{panic::{catch_unwind, AssertUnwindSafe}, usize}; -use std::rc::Rc; -use core::cell::RefCell; -use core::fmt::Write; - -/// Application Virtual Machine (AVM) - the main orchestrator for smart contract execution. -/// -/// EDUCATIONAL PURPOSE: This struct represents a complete blockchain virtual machine -/// that can execute smart contracts. It's similar to Ethereum's EVM or other blockchain VMs. -/// -/// AVM ARCHITECTURE OVERVIEW: -/// - Context Stack: Manages nested contract calls (like a call stack in programming) -/// - Memory Manager: Allocates and manages memory pages for contract execution -/// - Storage: Global persistent storage shared across all contracts -/// - State: Manages accounts, balances, and contract code -/// -/// BLOCKCHAIN CONCEPTS: -/// - Each contract has its own account with code and storage -/// - Transactions can create accounts or call existing contracts -/// - Contracts can call other contracts (nested execution) -/// - All state changes are atomic (all succeed or all fail) -/// -/// REAL-WORLD BLOCKCHAIN COMPARISON: -/// This AVM is inspired by Ethereum's EVM but simplified for educational purposes: -/// - Ethereum has more complex gas accounting and pricing -/// - Real blockchains have more sophisticated memory management -/// - Production VMs include additional security features like reentrancy protection -/// - Gas limits and execution timeouts prevent infinite loops -/// -/// VIRTUAL MACHINE LAYERS: -/// The AVM operates at multiple abstraction levels: -/// 1. Transaction Layer: Processes blockchain transactions -/// 2. Contract Layer: Executes smart contract bytecode -/// 3. Memory Layer: Manages contract memory allocation -/// 4. Storage Layer: Provides persistent data storage -/// 5. State Layer: Maintains global blockchain state -/// -/// SECURITY CONSIDERATIONS: -/// - Panic handling prevents one bad contract from crashing the entire system -/// - Memory isolation between contracts prevents interference -/// - Input validation prevents resource exhaustion attacks -/// - Context tracking prevents unauthorized cross-contract access -pub struct AVM { - /// Stack of execution contexts for nested contract calls. - /// - /// EDUCATIONAL: This implements a call stack similar to how functions - /// call other functions in programming. Each context tracks who called - /// whom and with what data. This is crucial for debugging and gas accounting. - pub context_stack: ContextStack, - - /// Manages allocation of memory pages for contract execution. - /// - /// EDUCATIONAL: Each contract gets its own memory page to prevent - /// interference between contracts. This is like process isolation in - /// operating systems - one process can't access another's memory. - pub memory_manager: MemoryPageManager, - - /// Global state of the AVM including all accounts and their data. - /// - /// EDUCATIONAL: This represents the entire blockchain state - all - /// accounts, their balances, code, and storage. Every transaction - /// can potentially modify this state. - pub state: State, - - pub verbose: bool, // Enable verbose logging for debugging - - /// Optional writer for verbose output. If None, outputs to console. - pub verbose_writer: Option>>, -} - -impl std::fmt::Debug for AVM { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AVM") - .field("context_stack", &self.context_stack) - .field("memory_manager", &self.memory_manager) - .field("state", &self.state) - .field("verbose", &self.verbose) - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) - .finish() - } -} - -impl AVM { - pub fn set_verbosity(&mut self, value: bool) { - self.verbose = value; - } - - /// Sets the output writer for verbose logging. - /// If set, verbose output will be written to this writer instead of console. - pub fn set_verbose_writer(&mut self, writer: Rc>) { - self.verbose_writer = Some(writer); - } - - /// Helper method to log output to either console or the configured writer - /// Only logs if verbose is true and self.verbose is enabled - fn log(&self, message: &str, verbose: bool) { - // Only log if this is not a verbose message, or if verbose logging is enabled - if verbose && !self.verbose { - return; - } - - match &self.verbose_writer { - Some(writer) => { - // Write to the provided writer (add newline manually) - let _ = write!(writer.borrow_mut(), "{}\n", message); - } - None => { - // Output to console - println!("{}", message); - } - } - } - - /// Creates a new Application Virtual Machine with specified memory constraints. - /// - /// EDUCATIONAL PURPOSE: This demonstrates VM initialization with resource limits. - /// In blockchain systems, resource limits are crucial to prevent denial-of-service - /// attacks and ensure predictable execution costs. - /// - /// RESOURCE MANAGEMENT: - /// - max_pages: Maximum number of memory pages that can be allocated - /// - page_size: Size of each memory page in bytes - /// - /// INITIALIZATION: All components start in a clean state, ready to - /// process transactions and execute contracts. - pub fn new(max_pages: usize, page_size: usize, state: State) -> Self { - Self { - context_stack: ContextStack::new(), - memory_manager: MemoryPageManager::new(max_pages, page_size), - state, - verbose: false, // Default to no verbose logging - verbose_writer: None, // Default to console output - } - } - - /// Executes a transaction, which can be a transfer, account creation, or contract call. - /// - /// EDUCATIONAL PURPOSE: This is the main entry point for processing blockchain - /// transactions. Each transaction type has different semantics and security considerations. - /// - /// TRANSACTION TYPES: - /// - Transfer: Move value between accounts (not implemented in this VM) - /// - CreateAccount: Deploy a new smart contract - /// - ProgramCall: Execute an existing smart contract - /// - /// TRANSACTION PROCESSING FLOW: - /// 1. Validate transaction format and parameters - /// 2. Check account existence and permissions - /// 3. Execute the appropriate operation based on transaction type - /// 4. Update global state with the results - /// 5. Return success/failure status - /// - /// ATOMICITY: All state changes within a transaction are atomic - either - /// all succeed or all fail. This ensures data consistency even if the - /// system crashes during transaction processing. - /// - /// ERROR HANDLING: Uses catch_unwind to prevent panics from crashing the entire - /// system. This is crucial in blockchain systems where one bad transaction - /// shouldn't affect others. - /// - /// GAS ACCOUNTING: In real blockchains, each operation costs gas, and - /// transactions have gas limits. This implementation is simplified and - /// doesn't include gas accounting. - /// - /// RETURN VALUE: Returns a Result indicating success/failure and any error codes - pub fn run_tx(&mut self, tx: Transaction) -> TransactionReceipt { - match tx.tx_type { - TransactionType::Transfer => { - // EDUCATIONAL: Value transfer between accounts - let ok = self.apply_transfer(tx.from, tx.to, tx.value); - return TransactionReceipt::new(tx, Result::new(ok, if ok { 0 } else { 1 })); - } - - TransactionType::CreateAccount => { - let result = catch_unwind(AssertUnwindSafe(|| { - self.create_account(tx.from, tx.to, tx.data.clone()); - })); - - // EDUCATIONAL: Handle deployment failures gracefully - if let Err(_e) = result { - return TransactionReceipt::new(tx, Result::new(false, 1)); - } else { - return TransactionReceipt::new(tx, Result::new(true, 0)); - } - } - - TransactionType::ProgramCall => { - // EDUCATIONAL: Execute an existing smart contract - // First verify the destination is actually a contract - assert!(self.state.is_contract(tx.to), "destination address is not a contract"); - - // EDUCATIONAL: Call the contract and extract the result - let (result_ptr, context_index) = self.call_contract(tx.from, tx.to, tx.data.clone()); - - // verify context stack is empty - if !self.context_stack.is_empty() { - if self.context_stack.iter().any(|ctx| !ctx.exe_done) { - panic!("context stack has unfinished contexts after tx execution"); - } - } - - // extract result - let res = self.extract_result(result_ptr, context_index); - TransactionReceipt::new(tx, res) - // Include events from this context and any nested calls. - .set_events(self.context_stack.collect_events_from(context_index)) - } - } - } - - /// Moves native tokens between two accounts. Returns true on success. - pub fn apply_transfer(&mut self, from: Address, to: Address, amount: u64) -> bool { - let amount = amount as u128; - let from_account = self.state.get_account_mut(&from); - if from_account.balance < amount { - return false; - } - from_account.balance -= amount; - - let to_account = self.state.get_account_mut(&to); - to_account.balance = to_account.balance.saturating_add(amount); - true - } - - /// Extracts the result of a contract execution from memory. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how contract results are communicated - /// back to the caller. The contract writes its result to a specific memory location, - /// and this function reads it. - /// - /// RESULT FORMAT: The result is stored as a 261-byte structure: - /// - 1 byte: success flag (0 = false, non-zero = true) - /// - 4 bytes: error code (u32) - /// - 4 bytes: data length (u32) - /// - 256 bytes: data array - /// - /// MEMORY SAFETY: Validates that the result pointer is within bounds - /// to prevent reading invalid memory. - fn extract_result(&self, _result_ptr: u32, context_index: usize) -> Result { - // EDUCATIONAL: Get the memory page where the result was stored - let ee = self.context_stack.get(context_index).expect("missing execution context"); - let vm = ee.vm.borrow(); - let page = vm.memory.borrow(); - - // EDUCATIONAL: Use the memory page's offset calculation to get the correct memory location - let start = page.offset(Config::RESULT_ADDR as usize); // Use memory page offset - - // EDUCATIONAL: Validate memory bounds to prevent out-of-bounds access - if start + Config::MAX_RESULT_SIZE > page.size() { - panic!("Result struct out of bounds at 0x{:08x}", start); - } - - // EDUCATIONAL: Extract the result fields from memory using the correct offset - let mem = page.mem(); - - let success = mem[start] != 0; - let error_code = u32::from_le_bytes(mem[start+1..start + 5].try_into().unwrap()); - let data_len = u32::from_le_bytes(mem[start+5..start + 9].try_into().unwrap()); - - // EDUCATIONAL: Extract the data array - let mut data = [0u8; 256]; - data.copy_from_slice(&mem[start+9..start + 265]); - - return Result { success, error_code, data_len, data }; - } - - /// Creates a new account (smart contract) with the provided code. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract deployment. - /// In blockchain systems, deploying a contract creates a new account - /// that can hold code and persistent storage. - /// - /// SECURITY CHECKS: - /// - Ensures the target address isn't already in use - /// - Validates code size limits to prevent resource exhaustion - /// - /// ACCOUNT CREATION: Creates an Account struct with: - /// - code: The smart contract bytecode - /// - storage: Empty persistent storage - /// - balance: 0 (no initial funds) - /// - nonce: 0 (no transactions yet) - /// - is_contract: true (marks this as a contract account) - pub fn create_account(&mut self, _from: Address, to: Address, data: Vec) { - // EDUCATIONAL: Deploy a new smart contract - // This creates a new account with the provided code - let is_contract = !data.is_empty(); - let code_size = data.len(); - - self.log(&format!( - "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", - to, - is_contract, - code_size - ), false); - - // EDUCATIONAL: Check that the target address is not already in use - // This prevents overwriting existing accounts - if self.state.accounts.contains_key(&to) { - panic!("account already exists"); - } - - // EDUCATIONAL: Validate code size limits - // This prevents resource exhaustion attacks - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; - if data.len() > max { - panic!( - "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", - data.len(), - max - ); - } - - // EDUCATIONAL: Create and insert new account with code - let account = Account { - code: data, // The smart contract bytecode - storage: Default::default(), // Empty persistent storage - balance: 0, // No initial balance - nonce: 0, // No transactions yet - is_contract: true, // Mark as contract account - }; - - self.state.accounts.insert(to, account); - } - - /// Handles calling a new contract, spinning up a fresh VM with its own memory page. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract execution. - /// Each contract call gets its own isolated VM instance to prevent - /// interference between contracts. - /// - /// EXECUTION PROCESS: - /// 1. Validate the target is a contract - /// 2. Allocate fresh memory and storage - /// 3. Set up the VM with contract code and parameters - /// 4. Execute the contract safely - /// 5. Extract and return the result - /// - /// ISOLATION: Each contract gets its own memory page and storage - /// to prevent one contract from affecting another. - /// - /// PARAMETER PASSING: Contract parameters are passed through CPU registers: - /// - a0: Contract address (who is being called) - /// - a1: Caller address (who is making the call) - /// - a2: Input data pointer - /// - a3: Input data length - /// - a4: Result pointer (where to write the result) - pub fn call_contract(&mut self, from: Address, to: Address, input_data: Vec) -> (u32, usize) { - self.log(&format!( - "Tx calling program at address {} with data 0x{}", - to, - hex::encode(&input_data) - ), false); - - // Save address for later use in termination log - let to_addr_str = to.to_string(); - - // SAFETY NOTE: - // This line creates a HostShim containing a raw pointer (*mut AVM) to self. - // Even though raw pointers don't participate in Rust's borrow checker, - // calling `HostShim::new(self)` still *temporarily borrows* `self` as `&mut AVM` - // during this line. If `self` is already mutably borrowed (e.g. for pushing to context_stack, - // accessing state, or memory_manager), this will cause a compile-time error due to overlapping mutable borrows. - // To avoid this, ensure all other mutable uses of `self` happen *before* or *after* this line. - let shim = HostShim::new(self); - - // EDUCATIONAL: Get mutable reference to the contract account - let account = self.state.get_account_mut(&to); - if !account.is_contract { - panic!("destination address {} is not a contract", to); - } - - // EDUCATIONAL: Allocate memory and clone storage for isolation - let memory_page = self.memory_manager.new_page(); - let storage = Rc::new(RefCell::new(Storage::with_map(account.storage.clone()))); - - // EDUCATIONAL: Create and configure child VM - // We use Box here to heap-allocate the HostShim and pass it as a trait object (Box). - // This is necessary because `VM` stores the host as `Box`, which: - // - Allows us to erase the concrete type (HostShim) at compile time - // - Removes the need for lifetimes like &'a mut dyn HostInterface - // - Enables recursive call_contract logic, since the Box owns the host and doesn't borrow `self` - // Without Box, we would need to track lifetimes manually and would hit borrow checker issues. - let mut vm: VM = VM::new_with_writer(memory_page, storage.clone(), Box::new(shim), self.verbose_writer.clone()); - vm.set_code(0, Config::PROGRAM_START_ADDR, &account.code); - vm.cpu.verbose = self.verbose; - - // Set up logging writer for CPU to use AVM's logging mechanism - if let Some(writer) = &self.verbose_writer { - vm.cpu.set_verbose_writer(writer.clone()); - } - - // add new context execution - let context_index = self.context_stack.push(from, to, input_data, vm); - let context = self.context_stack.current_mut().expect("missing execution context"); - - // EDUCATIONAL: Set up function parameters in registers - // This follows the RISC-V calling convention - let _address_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A0, to.0.as_ref()); // Contract address - let _pubkey_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A1, from.0.as_ref()); // Caller address - - // EDUCATIONAL: Validate input size to prevent resource exhaustion - let input_len = context.input_data.len(); - if input_len > Config::MAX_INPUT_LEN { - panic!( - "Entrypoint: input length {} exceeds MAX_INPUT_LEN ({})", - input_len, - Config::MAX_INPUT_LEN - ); - } - - // EDUCATIONAL: Set up input data (no result pointer needed) - let _input_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A2, &context.input_data); // Input data - context.vm.borrow_mut().set_reg_u32(Register::A3, input_len as u32); // Input length - - // EDUCATIONAL: Run the VM safely with panic handling - let result = catch_unwind(AssertUnwindSafe(|| { - context.vm.borrow_mut().raw_run(); - })); - - // EDUCATIONAL: Handle VM panics gracefully - if let Err(e) = result { - eprintln!("πŸ’₯ VM panicked: {:?}", e); - panic!("VM panicked"); - } - - // EDUCATIONAL: Copy storage back into account - // This persists any changes the contract made to storage - let updated_map = storage.borrow().map.borrow().clone(); - account.storage = updated_map; - - // EDUCATIONAL: set context execution done - context.exe_done = true; - - // Log execution termination for binary comparison tracking (after all borrows are done) - self.log(&format!("Execution terminated for address {}", to_addr_str), false); - - (Config::RESULT_ADDR, context_index) // Fixed result address - } - - /// Peek the current active execution context. - /// - /// EDUCATIONAL PURPOSE: This allows inspection of the current execution - /// context, which is useful for debugging and understanding the call stack. - /// - /// USAGE: Typically used by debugging tools or for implementing features - /// like gas accounting or call tracing. - pub fn current_context(&self) -> Option<&ExecutionContext> { - self.context_stack.current() - } -} diff --git a/crates/avm/src/execution_context.rs b/crates/avm/src/execution_context.rs deleted file mode 100644 index ea262b8..0000000 --- a/crates/avm/src/execution_context.rs +++ /dev/null @@ -1,114 +0,0 @@ -use types::address::Address; -use std::rc::Rc; -use std::cell::RefCell; -use vm::vm::VM; - -/// Represents a single execution context during contract calls. -#[derive(Debug, Clone)] -pub struct ExecutionContext { - /// The address that initiated the current call. - pub from: Address, - - /// The address currently receiving the call. - pub to: Address, - - // Data passed to the contract call - pub input_data: Rc>, - - // Memory page - pub vm: Rc>, - - pub events: Vec>, - - // is exe_done marks context as executed - pub exe_done: bool, -} - -impl ExecutionContext { - pub fn new( - from: Address, - to: Address, - input_data: Vec, - vm: VM, - ) -> Self { - Self { - from, - to, - input_data: Rc::new(input_data), - vm: Rc::new(RefCell::new(vm)), - events: Vec::new(), - exe_done: false, - } - } -} - -/// A call stack for nested execution contexts in the VM. -#[derive(Debug)] -pub struct ContextStack { - stack: Vec, -} - -impl ContextStack { - /// Create a new, empty context stack. - pub fn new() -> Self { - Self { stack: Vec::new() } - } - - /// Push a new context onto the stack (e.g., when a contract calls another). - /// returns index of the new execution context - pub fn push(&mut self, from: Address, to: Address, input_data: Vec, vm: VM) -> usize { - let index = self.stack.len(); - self.stack.push( - ExecutionContext { - from, - to, - input_data:Rc::new(input_data), - vm:Rc::new(RefCell::new(vm)), - events: Vec::new(), - exe_done: false, - }); - index - } - - /// Pop the most recent context off the stack (e.g., when returning from a call). - pub fn pop(&mut self) -> Option { - self.stack.pop() - } - - /// Peek execution context index without modifying the stack. - pub fn get(&self, i: usize) -> Option<&ExecutionContext> { - self.stack.get(i) - } - - pub fn get_mut(&mut self, i: usize) -> Option<&mut ExecutionContext> { - self.stack.get_mut(i) - } - - /// Peek at the current execution context without modifying the stack. - pub fn current(&self) -> Option<&ExecutionContext> { - self.stack.last() - } - - pub fn current_mut(&mut self) -> Option<&mut ExecutionContext> { - self.stack.last_mut() - } - - - pub fn iter(&self) -> impl Iterator { - self.stack.iter() - } - - pub fn is_empty(&self) -> bool { - self.stack.is_empty() - } - - /// Collect all events from a starting context index through the top of the stack. - pub fn collect_events_from(&self, start: usize) -> Vec> { - self.stack - .iter() - .enumerate() - .filter(|(idx, _)| *idx >= start) - .flat_map(|(_, ctx)| ctx.events.clone()) - .collect() - } -} diff --git a/crates/avm/src/global.rs b/crates/avm/src/global.rs deleted file mode 100644 index fb52c51..0000000 --- a/crates/avm/src/global.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub struct Config; - -impl Config { - pub const MAX_INPUT_LEN: usize = 1024; - pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries - pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data - pub const HEAP_START_ADDR: usize = Self::CODE_SIZE_LIMIT + Self::RO_DATA_SIZE_LIMIT + 0x100; - pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; - - pub const PROGRAM_START_ADDR: u32 = 0x400; - pub const RESULT_ADDR: u32 = 0x100; -} diff --git a/crates/avm/src/host_interface.rs b/crates/avm/src/host_interface.rs deleted file mode 100644 index 6a487a3..0000000 --- a/crates/avm/src/host_interface.rs +++ /dev/null @@ -1,102 +0,0 @@ -use vm::host_interface::HostInterface; -use types::address::Address; -use crate::avm::AVM; - -// HostShim is a lightweight adapter that allows a VM to call back into the AVM. -// It implements the HostInterface trait and holds a raw pointer to the AVM. -// -// We use a raw pointer (*mut AVM) instead of &'a mut AVM to avoid borrow checker conflicts. -// This is necessary because AVM::call_contract creates new VMs, which also require access -// to the AVM via HostInterface. If we used &'a mut AVM, we’d get lifetime or multiple mutable -// borrow errors due to recursive calls. -// -// By using *mut AVM: -// - We avoid tracked mutable borrows -// - We preserve safety by ensuring the pointer is only dereferenced during the call -// - We allow recursive VM execution without violating Rust's ownership model -// -// This approach is safe in our case because: -// - Each VM invocation gets its own HostShim -// - The pointer never escapes its VM or outlives AVM -// - We do not access AVM concurrently or from multiple threads -#[derive(Debug)] -pub struct HostShim { - pub avm_ptr: *mut AVM, // raw pointer to the AVM -} - -impl HostShim { - pub fn new(avm: &mut AVM) -> Self { - HostShim { - avm_ptr: avm as *mut AVM, - } - } -} - -impl<'a> HostInterface for HostShim { - fn call_program(&mut self, from: [u8; 20], to: [u8; 20], input_data: Vec) -> (u32, usize) { - unsafe { - return (*self.avm_ptr).call_contract(Address(from), Address(to), input_data); - } - } - - fn fire_event(&mut self, event: Vec) { - unsafe { - // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory - let avm = &mut *self.avm_ptr; - avm.context_stack.current_mut().expect("must have current context").events.push(event.clone()); - - let hex_string: String = event - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::>() - .join(" "); - - println!("[sys_fire_event] Event bytes (hex): {}", hex_string); - } - } - - fn read_memory_page(&mut self, page_index: usize, guest_ptr: u32, len: usize) -> Option> { - unsafe { - // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory - let avm = &*self.avm_ptr; - - let ee = avm.context_stack.get(page_index).expect("missing execution context"); - let vm = ee.vm.borrow(); - let page_ref = vm.memory.borrow(); - - // Assume the callee's memory manager is accessible here - let mem = page_ref.mem(); - - let start = guest_ptr as usize; - let end = start.checked_add(len)?; - - if end > mem.len() { - return None; // Out of bounds - } - - Some(mem[start..end].to_vec()) - } - } - - fn transfer(&mut self, to: [u8; 20], value: u64) -> bool { - unsafe { - let avm = &mut *self.avm_ptr; - let to_addr = Address(to); - - // Use the active execution context to determine the sender. - let ctx = match avm.context_stack.current() { - Some(c) => c, - None => return false, - }; - avm.apply_transfer(ctx.from, to_addr, value) - } - } - - fn balance(&mut self, addr: [u8; 20]) -> u128 { - unsafe { - let avm = &mut *self.avm_ptr; - let account = avm.state.get_account(&Address(addr)); - account.map(|a| a.balance).unwrap_or(0) - } - } -} diff --git a/crates/avm/src/lib.rs b/crates/avm/src/lib.rs deleted file mode 100644 index f4361a3..0000000 --- a/crates/avm/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -// external -pub extern crate hex; - -// exports -pub mod avm; -pub mod transaction; -pub mod memory_page_manager; -pub mod global; -pub mod execution_context; -pub mod router; -pub mod host_interface; -pub mod receipt; \ No newline at end of file diff --git a/crates/avm/src/memory_page_manager.rs b/crates/avm/src/memory_page_manager.rs deleted file mode 100644 index 07f0f7b..0000000 --- a/crates/avm/src/memory_page_manager.rs +++ /dev/null @@ -1,90 +0,0 @@ -use vm::memory_page::MemoryPage; -use std::{cell::RefCell, rc::Rc}; - -#[derive(Debug)] -pub struct MemoryPageManager { - pub page_size: usize, - max_pages: usize, - pages: Vec>>, -} - -impl MemoryPageManager { - pub fn new(max_pages: usize, page_size: usize) -> Self { - assert!(max_pages != 0, "Max pages == 0"); - assert!(page_size != 0, "Page size == 0"); - - Self { - page_size, - max_pages, - pages: Vec::with_capacity(max_pages), - } - } - - /// Creates and owns a new page. Returns a mutable reference to it. - pub fn new_page(&mut self) -> Rc> { - if self.pages.len() >= self.max_pages { - panic!("Out of memory: maximum page count ({}) reached", self.max_pages); - } - - let page = Rc::new(RefCell::new(MemoryPage::new(self.page_size))); - self.pages.push(Rc::clone(&page)); - return page; - } - - pub fn pop_page(&mut self) { - self.pages.pop(); - } - - /// Pretty-prints all memory pages linearly, indicating page boundaries - pub fn dump_all_pages_linear(&self) { - println!("Dumping memory ({} pages):", self.pages.len()); - for (i, page_rc) in self.pages.iter().enumerate() { - println!("\n=== Page {} ===", i); - - let page = page_rc.borrow(); - let mem = page.mem(); - - for (j, chunk) in mem.chunks(16).enumerate() { - print!("0x{:04x}: ", j * 16); - - // Print hex representation - for byte in chunk { - print!("{:02x} ", byte); - } - - // Pad spacing if chunk is less than 16 bytes - for _ in chunk.len()..16 { - print!(" "); - } - - // Print ASCII representation - print!(" |"); - for byte in chunk { - let ch = *byte; - let display_char = if ch.is_ascii_graphic() || ch == b' ' { - ch as char - } else { - '.' - }; - print!("{}", display_char); - } - println!("|"); - } - } -} - - - - pub fn get_page(&self, index: usize) -> Option>> { - self.pages.get(index).cloned() // βœ… clone the Rc (increases refcount) - } - - pub fn first_page(&self) -> Option>> { - self.pages.first().cloned() // βœ… clone the Rc (increases refcount) - } - - pub fn top_page(&self) -> Option>> { - self.pages.last().cloned() // βœ… clone the Rc (increases refcount) - } - -} diff --git a/crates/avm/src/receipt.rs b/crates/avm/src/receipt.rs deleted file mode 100644 index dd40942..0000000 --- a/crates/avm/src/receipt.rs +++ /dev/null @@ -1,189 +0,0 @@ -use types::{Result}; -use crate::transaction::Transaction; - -/// Represents the result of a transaction execution. -#[derive(Debug, Clone)] -pub struct TransactionReceipt { - /// Hash of the transaction. - pub tx: Transaction, - - /// Cumulative gas used in the block including this transaction. - // pub cumulative_gas_used: u64, - - // /// Gas used by this transaction alone. - // pub gas_used: u64, - - pub result: Result, - - /// List of log entries generated during execution. - pub events: Vec>, -} - -impl TransactionReceipt { - /// Creates a new TransactionReceipt. - pub fn new(tx: Transaction, result: Result) -> Self { - TransactionReceipt { - tx, - // cumulative_gas_used: 0, - // gas_used: 0, - result, - events: Vec::new(), - } - } - - /// Adds an event to the receipt. - pub fn add_event(&mut self, event: Vec) -> &TransactionReceipt { - self.events.push(event); - self - } - - /// Optionally add multiple events at once. - pub fn set_events(mut self, events: Vec>) -> Self { - self.events = events; - self - } -} - -use core::fmt; - -impl fmt::Display for TransactionReceipt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "=== Transaction Receipt ===")?; - writeln!(f, "From: {:?}", self.tx.from)?; - writeln!(f, "To: {:?}", self.tx.to)?; - writeln!(f, "Result: {:?}", self.result)?; - writeln!(f, "Events:")?; - - for (i, event) in self.events.iter().enumerate() { - let hex = event.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); - writeln!(f, " [{}] {}", i, hex)?; - } - - Ok(()) - } -} - -use compiler::{EventAbi, ParamType}; - -impl TransactionReceipt { - pub fn print_events_pretty(&self, abi_registry: &Vec, writer: &mut dyn fmt::Write) { - if self.events.is_empty() { - let _ = writeln!(writer, "No events in receipt."); - return; - } - - for event in &self.events { - Self::pretty_print_event(event, abi_registry, writer); - } - let _ = writeln!(writer); - } - - pub fn pretty_print_event(event: &[u8], abi_registry: &Vec, writer: &mut dyn fmt::Write) { - if event.len() < 32 { - let _ = writeln!(writer, "Invalid event: too short"); - return; - } - - let mut id = [0u8; 32]; - id.copy_from_slice(&event[..32]); - let data = &event[32..]; - - if let Some(abi) = abi_registry.iter().find(|abi| abi.id() == id) { - let _ = writeln!(writer, " {}: (", abi.name); - let mut offset = 0; - - let _ = writeln!(writer, " ID: 0x{}", hex::encode(id)); - for (i, param) in abi.inputs.iter().enumerate() { - let val = if param.indexed { - "".to_string() - } else { - match param.kind { - ParamType::Address => { - if offset + 20 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes: &[u8] = &data[offset..offset + 20]; - offset += 20; - format!("0x{}", hex::encode(bytes)) - } - ParamType::Uint(256) => { - if offset + 32 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes = &data[offset..offset + 32]; - offset += 32; - format!("0x{}", hex::encode(bytes)) - } - ParamType::Uint(128) => { - if offset + 16 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes = &data[offset..offset + 16]; - offset += 16; - let raw = u128::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Uint(64) => { - if offset + 8 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes = &data[offset..offset + 8]; - offset += 8; - let raw = u64::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Uint(32) => { - if offset + 4 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes = &data[offset..offset + 4]; - offset += 4; - let raw = u32::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Bool => { - if offset + 1 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let b = data[offset]; - offset += 1; - format!("{}", b != 0) - } - ParamType::Bytes => { - if offset + 1 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let len = data[offset] as usize; - offset += 1; - if offset + len > data.len() { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - let bytes = &data[offset..offset + len]; - offset += len; - format!("0x{}", hex::encode(bytes)) - } - _ => { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - } - }; - - let comma = if i + 1 < abi.inputs.len() { "," } else { "" }; - let _ = writeln!(writer, "\t{}: {}{}", param.name, val, comma); - } - - let _ = writeln!(writer, " )"); - } else { - let _ = writeln!(writer, "Unknown event: 0x{}", hex::encode(id)); - } - } -} diff --git a/crates/avm/src/router.rs b/crates/avm/src/router.rs deleted file mode 100644 index 933661e..0000000 --- a/crates/avm/src/router.rs +++ /dev/null @@ -1,21 +0,0 @@ -/// Represents a function call input for the VM router -pub struct HostFuncCall { - pub selector: u8, - pub args: Vec, -} - -/// Encodes multiple function calls into a single buffer for the guest VM router. -pub fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { - let mut encoded = Vec::new(); - - for call in calls { - let len = call.args.len(); - assert!(len <= 255, "argument too long for 1-byte length field"); - - encoded.push(call.selector); - encoded.push(len as u8); - encoded.extend_from_slice(&call.args); - } - - encoded -} diff --git a/crates/avm/src/transaction.rs b/crates/avm/src/transaction.rs deleted file mode 100644 index 932d0214..0000000 --- a/crates/avm/src/transaction.rs +++ /dev/null @@ -1,47 +0,0 @@ -use types::address::Address; - -#[derive(Debug, Clone)] -pub enum TransactionType { - /// Type 0 - Regular value transfer (not a contract) - Transfer = 0, - - /// Type 1 - Account create with program data (contract deployment) - CreateAccount = 1, - - /// Type 2 - Contract call (calling into existing code) - ProgramCall = 2, -} - -#[derive(Debug, Clone)] -pub struct Transaction { - pub tx_type: TransactionType, // type of transaction - pub to: Address, // recipient address - pub from: Address, // sender public key/address - pub data: Vec, // input data - pub value: u64, // amount/value sent - pub nonce: u64, // transaction nonce -} - -/// Holds a set of transactions to be processed as a unit -#[derive(Debug, Clone)] -pub struct TransactionBundle { - pub transactions: Vec, -} - -impl TransactionBundle { - pub fn new(transactions: Vec) -> Self { - TransactionBundle { transactions } - } - - pub fn add_transaction(&mut self, tx: Transaction) { - self.transactions.push(tx); - } - - pub fn len(&self) -> usize { - self.transactions.len() - } - - pub fn is_empty(&self) -> bool { - self.transactions.is_empty() - } -} \ No newline at end of file diff --git a/crates/bootloader/.gitignore b/crates/bootloader/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/crates/bootloader/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/crates/bootloader/Cargo.toml b/crates/bootloader/Cargo.toml new file mode 100644 index 0000000..8cedb4d --- /dev/null +++ b/crates/bootloader/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bootloader" +version = "0.1.0" +edition = "2024" +readme = "README.md" + +[features] +guest_kernel = [] + +[dependencies] +types = { path = "../types" } + +[target.'cfg(not(target_os = "none"))'.dependencies] +vm = { path = "../vm" } +state = { path = "../state" } +goblin = "0.10" +compiler = { path = "../compiler" } +clibc = { path = "../clibc" } + +[target.'cfg(target_arch = "riscv32")'.dependencies] +clibc = { path = "../clibc", default-features = false } diff --git a/crates/bootloader/README.md b/crates/bootloader/README.md new file mode 100644 index 0000000..b6eb703 --- /dev/null +++ b/crates/bootloader/README.md @@ -0,0 +1,58 @@ +# OS (Alon's OS) + +Alon's OS (OS) is a minimal operating system purpose-built for deterministic, blockchain-style program execution. It replaces the current `avm` crate with a layered OS: a bootloader for trust establishment, a kernel that orchestrates stateful execution, and `liba`, the standard library that application programs link against. + +## Goals +- Deterministic, replayable execution for consensus environments +- Small, auditable surface area with a clear chain of trust from ROM to apps +- Opinionated syscall and runtime model tailored for blockchain state transitions +- First-class support for smart-contract style programs through `liba` +- Ergonomic developer experience while keeping kernel/runtime minimal + +## Layered Architecture +1. **Bootloader**: First-stage loader that verifies the kernel and `liba` images, measures them, and passes a concise boot manifest to the kernel. Runs in a restricted environment with no dynamic allocation. +2. **Kernel**: Manages memory layout, page tables, capabilities, and the syscall surface. Provides deterministic scheduling and ties block context (slot, leader, parent state root) to every execution. +3. **Execution Runtime**: The blockchain-aware executor that will supersede `avm`. It coordinates transaction/block execution, drives the VM, and emits receipts and event logs. +4. **liba (Standard Library)**: Successor of the `clibc` crate, offering safe wrappers over syscalls (storage, logs, crypto, messaging), ABI helpers, and contract-to-contract call utilities. +5. **Tooling**: Compiler and host utilities reused from the existing workspace to build and package aOS images and applications. + +## Component Details +### Bootloader +- Validates and measures the kernel and `liba` artifacts before execution. +- Builds a `BootInfo` payload (memory map, entry points, config) handed to the kernel. +- Provides a minimal diagnostic console for early-boot errors. + +### Kernel +- Sets up paging and isolates execution contexts (per-transaction or per-program). +- Implements a deterministic scheduler and resource accounting suited for block production. +- Exposes a constrained syscall table: storage access, logging/events, crypto primitives, time/slot metadata, inter-program calls. +- Tracks state roots and receipts to keep execution verifiable. + +### Execution Runtime +- Drives the VM for each transaction, wiring block context into the kernel-provided syscalls. +- Applies state transitions via the `state` and `storage` crates and emits receipts for verification. +- Provides hooks for precompiles and deterministic host functions. + +### liba (Application Standard Library) +- Derived from the existing `clibc` crate and tailored to aOS. +- Offers ABI types, context helpers, and safe wrappers around syscalls exposed by the kernel. +- Ships default modules for storage, logging/events, cross-program calls, and crypto utilities. + +## Program Lifecycle (Happy Path) +1. Bootloader measures kernel + `liba`, builds `BootInfo`, and jumps to the kernel. +2. Kernel sets up memory and installs syscall table based on the boot manifest. +3. For each block, the runtime constructs execution contexts with block metadata and state snapshots. +4. Transactions enter the VM; `liba` mediates syscalls to kernel services. +5. State and receipts are persisted; the resulting state root/receipts are exposed to consensus. + +## Relationship to Existing Workspace +- `os` replaces the `avm` crate as the orchestrator/runtime. +- `clibc` is internalized as `liba` inside this crate (module structure will mirror the current APIs). +- `vm`, `state`, `storage`, `types`, and `compiler` remain the core building blocks for CPU execution, state transitions, persistence, shared types, and toolchain support. + +## Roadmap (Initial Steps) +- Define kernel <-> runtime <-> `liba` interfaces and shared types. +- Port `clibc` into `liba` and expose it as the supported app-facing API. +- Migrate `avm` responsibilities into the aOS runtime and deprecate `avm`. +- Add a boot manifest format and minimal bootloader stubs to validate and launch the kernel. +- Document syscall semantics and determinism guarantees for contract authors. diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs new file mode 100644 index 0000000..2c8937c --- /dev/null +++ b/crates/bootloader/src/bootloader.rs @@ -0,0 +1,240 @@ +use core::{mem, slice}; +use core::fmt::Write as FmtWrite; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use std::vec::Vec; + +use compiler::elf::parse_elf_from_bytes; +use goblin::elf::Elf; +use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE}; + +use state::State; +use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; +use vm::registers::Register; +use vm::vm::VM; + +const KERNEL_WINDOW_BYTES: usize = 4 * 1024 * 1024; +const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; + +/// Boot configuration options consumed by the loader. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BootConfig { + pub debug_console: bool, +} + +impl Default for BootConfig { + fn default() -> Self { + Self { + debug_console: true, + } + } +} + +/// Bootloader skeleton that loads a kernel image into fresh memory and +/// hands control to the kernel. +#[derive(Debug)] +pub struct Bootloader { + pub config: BootConfig, + memory: Rc, + heap_ptr: Rc>, +} + +impl Bootloader { + pub fn new(total_size_bytes: usize) -> Self { + Self { + config: BootConfig::default(), + memory: Rc::new(Sv32Memory::new(total_size_bytes, PAGE_SIZE)), + heap_ptr: Rc::new(Cell::new(0)), + } + } + + /// Load an ELF kernel image into a fresh page and return its entry point + backing memory. + pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, MmuRef) { + let elf = parse_elf_from_bytes(elf_bytes).expect("failed to parse kernel ELF"); + let entry_point = Elf::parse(elf_bytes) + .expect("failed to parse entry point") + .entry as u32; + + let (code, code_base) = elf.get_flat_code().expect("kernel ELF missing .text"); + let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); + let (bss, bss_base) = elf.get_flat_bss().unwrap_or((Vec::new(), code_base)); + println!( + "kernel elf: text_base=0x{:08x} text_len=0x{:x} ro_base=0x{:08x} ro_len=0x{:x} bss_base=0x{:08x} bss_len=0x{:x}", + code_base as u32, + code.len(), + ro_base as u32, + rodata.len(), + bss_base as u32, + bss.len() + ); + + let mut min_base = core::cmp::min(code_base, ro_base) as usize; + if !bss.is_empty() { + min_base = core::cmp::min(min_base, bss_base as usize); + } + let code_end = (code_base + code.len() as u64) as usize; + let ro_end = (ro_base + rodata.len() as u64) as usize; + let mut image_end = core::cmp::max(code_end, ro_end); + if !bss.is_empty() { + let bss_end = bss_base + .checked_add(bss.len() as u64) + .expect("bss end overflow") as usize; + image_end = core::cmp::max(image_end, bss_end); + } + let image_size = image_end.checked_sub(min_base).expect("invalid image size"); + + assert!( + image_end <= self.memory.size(), + "ELF image does not fit in mapped memory (need {}, have {})", + image_end, + self.memory.size() + ); + assert!( + KERNEL_WINDOW_BYTES <= self.memory.size(), + "kernel window exceeds physical memory (need {}, have {})", + KERNEL_WINDOW_BYTES, + self.memory.size() + ); + + // Flatten code + rodata into a single buffer and write once to set heap pointer properly. + let mut image = vec![0u8; image_size]; + let code_off = (code_base as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + let ro_off = (ro_base as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); + } + if !bss.is_empty() { + let bss_off = (bss_base as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } + + self.memory + .map_range(VirtualAddress(0), KERNEL_WINDOW_BYTES, Perms::rwx_kernel()); + self.memory + .write_bytes(VirtualAddress(min_base as u32), &image); + // Start the heap after the loaded image to avoid overwriting kernel text/rodata. + let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; + self.set_next_heap(heap_start); + // Map a direct window over all physical memory so the kernel can touch + // page tables after paging is enabled. + let mapped = self.memory.map_physical_range( + VirtualAddress(SV32_DIRECT_MAP_BASE), + 0, + self.memory.size(), + Perms::rw_kernel(), + ); + assert!(mapped, "failed to map kernel direct physical window"); + let memory: MmuRef = self.memory.clone(); + (entry_point, memory) + } + + /// Execute a transaction bundle by delegating to the kernel. This mirrors the + /// AVM entry point where the kernel is responsible for invoking programs. + pub fn execute_bundle( + &mut self, + kernel_elf: &[u8], + bundle: &TransactionBundle, + state: Rc>, + verbose: bool, + verbose_writer: Option>>, + ) -> Option { + let (entry_point, memory) = self.load_kernel(kernel_elf); + let mut vm = VM::new(memory.clone()); + vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); + vm.cpu.verbose = verbose; + if let Some(writer) = verbose_writer { + vm.cpu.set_verbose_writer(writer); + } + vm.cpu.pc = entry_point; + + self.place_bundle(&mut vm, bundle); + let encoded_state = state.borrow().encode(); + self.place_state(&mut vm, &encoded_state); + self.place_boot_info(&mut vm); + vm.raw_run(); + crate::result::read_kernel_result(&memory) + } + + fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { + let encoded = bundle.encode(); + let addr = self.place_data(vm, Register::A0, &encoded); + // Register a length hint so the kernel can bounds-check the payload. + vm.set_reg_u32(Register::A1, encoded.len() as u32); + // Keep heap aligned after our write. + self.set_next_heap( + (addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32, + ); + } + + fn place_state(&mut self, vm: &mut VM, state: &[u8]) { + let addr = self.place_data(vm, Register::A2, state); + vm.set_reg_u32(Register::A3, state.len() as u32); + self.set_next_heap( + (addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32, + ); + } + + fn place_boot_info(&mut self, vm: &mut VM) { + // For now the bootloader owns the page tables, so `root_ppn` is a placeholder (0). + let heap_start = self.ensure_heap_ptr(); + let aligned_heap = (heap_start + 7) & !7; + let boot_info_size = mem::size_of::() as u32; + let next_heap = aligned_heap + .checked_add(boot_info_size) + .and_then(|v| v.checked_add(HEAP_PTR_OFFSET)) + .expect("boot info heap pointer overflow"); + let boot_info = BootInfo::new( + self.memory.current_root() as u32, + KERNEL_STACK_TOP, + next_heap, + self.memory.size() as u32, + self.memory.next_free_ppn() as u32, + 0, + KERNEL_WINDOW_BYTES as u32, + ); + let bytes = unsafe { + slice::from_raw_parts( + &boot_info as *const BootInfo as *const u8, + mem::size_of::(), + ) + }; + let _addr = self.place_data(vm, Register::A4, bytes); + self.set_next_heap(next_heap); + } + + fn place_data(&self, vm: &mut VM, reg: Register, data: &[u8]) -> u32 { + let addr = self.alloc_on_heap(data).as_u32(); + vm.cpu.regs[reg as usize] = addr; + addr + } + + fn ensure_heap_ptr(&self) -> u32 { + let current = self.heap_ptr.get(); + if current == 0 { + self.heap_ptr.set(HEAP_PTR_OFFSET); + HEAP_PTR_OFFSET + } else { + current + } + } + + fn set_next_heap(&self, next: u32) { + self.heap_ptr.set(next); + } + + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { + let mut addr = self.ensure_heap_ptr(); + let align = 8u32; + addr = (addr + (align - 1)) & !(align - 1); + let end = addr + .checked_add(data.len() as u32) + .expect("heap allocation overflow"); + let start = VirtualAddress(addr); + self.memory.map_range(start, data.len(), Perms::rw_kernel()); + self.memory.write_bytes(start, data); + self.heap_ptr.set(end); + start + } + +} diff --git a/crates/bootloader/src/lib.rs b/crates/bootloader/src/lib.rs new file mode 100644 index 0000000..0bc7a2d --- /dev/null +++ b/crates/bootloader/src/lib.rs @@ -0,0 +1,11 @@ +#![cfg_attr(target_arch = "riscv32", no_std)] +//! Deterministic OS scaffold for blockchain-style execution. +//! +//! This crate provides a bootloader skeleton that: +//! - loads a kernel program into fresh pages, +//! - hands the loaded image off to a future kernel runtime. +//! +//! Memory utilities are provided by the VM crate. + +pub mod bootloader; +pub mod result; diff --git a/crates/bootloader/src/result.rs b/crates/bootloader/src/result.rs new file mode 100644 index 0000000..3ad14fa --- /dev/null +++ b/crates/bootloader/src/result.rs @@ -0,0 +1,49 @@ +use core::mem; + +use types::kernel_result::KERNEL_RESULT_ADDR; +use types::{KernelResult, TransactionReceipt}; +use state::State; +use vm::memory::{Memory as MmuRef, VirtualAddress}; + +pub struct KernelRunResult { + pub receipts: Vec, + pub state: Option, +} + +pub(crate) fn read_kernel_result(memory: &MmuRef) -> Option { + let header_size = mem::size_of::() as u32; + let header_end = KERNEL_RESULT_ADDR.checked_add(header_size)?; + let header_slice = memory.mem_slice( + VirtualAddress(KERNEL_RESULT_ADDR), + VirtualAddress(header_end), + )?; + let header_bytes = header_slice.as_ref(); + if header_bytes.len() < header_size as usize { + return None; + } + let receipts_ptr = u32::from_le_bytes(header_bytes[0..4].try_into().ok()?); + let receipts_len = u32::from_le_bytes(header_bytes[4..8].try_into().ok()?); + let state_ptr = u32::from_le_bytes(header_bytes[8..12].try_into().ok()?); + let state_len = u32::from_le_bytes(header_bytes[12..16].try_into().ok()?); + if receipts_ptr == 0 || receipts_len == 0 { + return None; + } + let receipts_end = receipts_ptr.checked_add(receipts_len)?; + let receipts_slice = memory.mem_slice( + VirtualAddress(receipts_ptr), + VirtualAddress(receipts_end), + )?; + let receipts = TransactionReceipt::decode_list(receipts_slice.as_ref())?; + + let state = if state_ptr != 0 && state_len != 0 { + let state_end = state_ptr.checked_add(state_len)?; + let state_slice = memory.mem_slice( + VirtualAddress(state_ptr), + VirtualAddress(state_end), + )?; + State::decode(state_slice.as_ref()) + } else { + None + }; + Some(KernelRunResult { receipts, state }) +} diff --git a/crates/clibc/Cargo.toml b/crates/clibc/Cargo.toml new file mode 100644 index 0000000..7206893 --- /dev/null +++ b/crates/clibc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clibc" +version = "0.1.0" +edition = "2024" + +[features] +guest = [] +kernel = [] + +[dependencies] +types = { path = "../types" } +state = { path = "../state", default-features = false } diff --git a/crates/clibc/README.md b/crates/clibc/README.md new file mode 100644 index 0000000..a24ba41 --- /dev/null +++ b/crates/clibc/README.md @@ -0,0 +1,42 @@ +# clibc (Chain Libc) + +`clibc` is the smart contract runtime library for the Rust VM. It provides the +guest-facing API surface for syscalls, ABI helpers, storage primitives, logging, +and cross-program calls. The crate is `no_std` and built to run inside the VM. + +## Features +- `guest`: APIs intended for contract code running inside the VM. +- `kernel`: helpers used by the kernel/runtime side. + +## Module Overview +- `allocator`: VM-backed global allocator (enabled for RISC-V guest builds). +- `call`: cross-program call helper (`call`). +- `entrypoint`: `entrypoint!` macro for defining contract entry functions. +- `event`: `event!` definitions plus `fire_event!` dispatch. +- `integers`: simple integer readers (e.g., `read_u32`). +- `log`: logging macros (`log!`, `logf!`, `concat!`, `concat_str!`) and + `BufferWriter`. +- `panic`: `vm_panic` helper and guest panic handler. +- `parser`: `DataParser` and `HexCodec` utilities, plus `hex_address!` macro. +- `router`: `decode_calls`, `route`, and `FuncCall` for ABI routing. +- `storage`: `persist_struct!` macro and `Persistent` helpers. +- `storage_map`: `StorageMap`, `StorageKey`, and `Map!` macro for typed domains. +- `syscalls`: shared syscall IDs (storage, events, allocation, transfer). +- `transfer`: `transfer`, `balance`, and convenience macros. + +## Macros and Helpers +- `entrypoint!`: declare a contract entry function with a consistent ABI. +- `persist_struct!`: generate storage-backed struct load/store helpers. +- `Map!`: declare a typed storage map domain with get/set helpers. +- `event!` and `fire_event!`: define events and emit them via syscall. +- `log!`/`logf!`: basic logging and formatted logging. +- `transfer!`/`balance!`: concise wrappers for token transfer and balance. +- `hex_address!`: compile-time address parsing helper. +- `require`: guard helper that aborts execution with `vm_panic` on failure. + +## Usage +Add the crate to your workspace and import it as `clibc`: + +```rust +use clibc::{entrypoint, log, logf}; +``` diff --git a/crates/program/src/allocator.rs b/crates/clibc/src/allocator.rs similarity index 100% rename from crates/program/src/allocator.rs rename to crates/clibc/src/allocator.rs diff --git a/crates/program/src/call.rs b/crates/clibc/src/call.rs similarity index 100% rename from crates/program/src/call.rs rename to crates/clibc/src/call.rs diff --git a/crates/program/src/entrypoint.rs b/crates/clibc/src/entrypoint.rs similarity index 97% rename from crates/program/src/entrypoint.rs rename to crates/clibc/src/entrypoint.rs index 6fc79a2..ff274ed 100644 --- a/crates/program/src/entrypoint.rs +++ b/crates/clibc/src/entrypoint.rs @@ -11,7 +11,7 @@ /// /// USAGE: Call this macro with the name of your main contract function: /// ```ignore -/// use program::entrypoint; +/// use clibc::entrypoint; /// entrypoint!(my_contract_function); /// ``` /// @@ -43,7 +43,7 @@ macro_rules! entrypoint { ) { // EDUCATIONAL: Write result directly to predetermined memory location // This prevents conflicts with macros that might overwrite A4 - // Must match Config::RESULT_ADDR in crates/avm/src/global.rs + // Must match global::RESULT_ADDR in crates/kernel/src/global.rs const RESULT_ADDR: usize = 0x100; // Predetermined memory address for result object // EDUCATIONAL: Convert raw pointer to contract address diff --git a/crates/program/src/event.rs b/crates/clibc/src/event.rs similarity index 100% rename from crates/program/src/event.rs rename to crates/clibc/src/event.rs diff --git a/crates/program/src/integers.rs b/crates/clibc/src/integers.rs similarity index 100% rename from crates/program/src/integers.rs rename to crates/clibc/src/integers.rs diff --git a/crates/program/src/lib.rs b/crates/clibc/src/lib.rs similarity index 87% rename from crates/program/src/lib.rs rename to crates/clibc/src/lib.rs index 42b3558..e16de65 100644 --- a/crates/program/src/lib.rs +++ b/crates/clibc/src/lib.rs @@ -20,6 +20,10 @@ pub mod transfer; pub use transfer::transfer; pub use transfer::balance; +// Syscall IDs +pub mod syscalls; +pub use syscalls::*; + // StorageMap pub mod storage_map; pub use storage_map::StorageMap; @@ -32,6 +36,14 @@ pub use event::*; // Logging macros pub mod log; pub use log::BufferWriter; +pub use log::CONSOLE_WRITE_ID; + +#[cfg(feature = "kernel")] +pub const LOG_PREFIX: &str = "πŸ›‘οΈ Kernel: "; +#[cfg(all(not(feature = "kernel"), feature = "guest"))] +pub const LOG_PREFIX: &str = "πŸ“œ Guest: "; +#[cfg(all(not(feature = "kernel"), not(feature = "guest")))] +pub const LOG_PREFIX: &str = ""; // Data parser pub mod parser; @@ -54,20 +66,17 @@ pub use storage::PERSISTENT_DOMAIN; // Allow `$crate::PERSISTENT_DOMAIN` in macr pub mod router; pub use router::{decode_calls, route, FuncCall}; -// Panic handling -mod panic; +// Panic helper (and panic handler when guest feature enabled) +pub mod panic; pub use panic::vm_panic; // Memory allocator pub mod allocator; -// Global allocator - automatically provides heap allocation for all guest programs -// Only enable for RISC-V target to avoid recursion on host -#[cfg(target_arch = "riscv32")] +#[cfg(all(target_arch = "riscv32", feature = "guest"))] #[global_allocator] static ALLOCATOR: allocator::VmAllocator = allocator::VmAllocator; - /* --------------------------- Assertion Utilities -------------------------- */ /// Aborts execution if condition is false, printing `msg`. diff --git a/crates/program/src/log.rs b/crates/clibc/src/log.rs similarity index 91% rename from crates/program/src/log.rs rename to crates/clibc/src/log.rs index 01da956..a560364 100644 --- a/crates/program/src/log.rs +++ b/crates/clibc/src/log.rs @@ -1,15 +1,19 @@ +pub const CONSOLE_WRITE_ID: u32 = 1000; + #[macro_export] macro_rules! logf_syscall { ($fmt_ptr:expr, $fmt_len:expr, $args_ptr:expr, $args_len:expr) => {{ #[cfg(target_arch = "riscv32")] unsafe { core::arch::asm!( - "li a7, 4", // syscall_log + "li a7, {console_write}", "ecall", in("a1") $fmt_ptr, in("a2") $fmt_len, in("a3") $args_ptr, in("a4") $args_len, + console_write = const $crate::CONSOLE_WRITE_ID, + clobber_abi("C"), ); } #[cfg(not(target_arch = "riscv32"))] @@ -24,6 +28,9 @@ macro_rules! logf { ($fmt:expr) => {{ // Handle both string literals and byte strings let fmt_bytes: &[u8] = $crate::as_bytes!($fmt); + let prefix_bytes = $crate::LOG_PREFIX.as_bytes(); + let mut fmt_buf = [0u8; 256]; + let fmt_bytes = $crate::concat_str!(fmt_buf, prefix_bytes, fmt_bytes); let fmt_ptr = fmt_bytes.as_ptr(); let fmt_len = fmt_bytes.len(); $crate::logf_syscall!(fmt_ptr, fmt_len, 0 as *const u32, 0usize); @@ -83,6 +90,9 @@ macro_rules! logf { )+ let fmt_bytes: &[u8] = $crate::as_bytes!($fmt); + let prefix_bytes = $crate::LOG_PREFIX.as_bytes(); + let mut fmt_buf = [0u8; 256]; + let fmt_bytes = $crate::concat_str!(fmt_buf, prefix_bytes, fmt_bytes); let fmt_ptr = fmt_bytes.as_ptr(); let fmt_len = fmt_bytes.len(); let args_ptr = args_buf.as_ptr(); @@ -201,4 +211,4 @@ impl<'a> core::fmt::Write for BufferWriter<'a> { Ok(()) } } -} \ No newline at end of file +} diff --git a/crates/clibc/src/panic.rs b/crates/clibc/src/panic.rs new file mode 100644 index 0000000..01c1974 --- /dev/null +++ b/crates/clibc/src/panic.rs @@ -0,0 +1,46 @@ +//! Panic helper and handler for guest programs. + +/// Trap into the host with a panic message. +#[inline(always)] +pub fn vm_panic(msg: &[u8]) -> ! { + #[cfg(target_arch = "riscv32")] + unsafe { + core::arch::asm!( + "li a7, 3", // SYSCALL_PANIC + "ecall", + in("a1") msg.as_ptr(), + in("a2") msg.len(), + options(noreturn), + ); + } + + #[cfg(not(target_arch = "riscv32"))] + { + panic!( + "vm_panic: {}", + core::str::from_utf8(msg).unwrap_or("") + ); + } +} + +/// Guest panic handler for RISC-V builds (only when guest feature enabled). +#[cfg(all(target_arch = "riscv32", feature = "guest"))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let mut buf = [0u8; 256]; + let len = { + let mut writer = crate::BufferWriter::new(&mut buf); + if write!(&mut writer, "{}", info).is_ok() { + writer.len() + } else { + 0 + } + }; + if len == 0 { + vm_panic(b"guest panic"); + } else { + vm_panic(&buf[..len]); + } +} diff --git a/crates/program/src/parser.rs b/crates/clibc/src/parser.rs similarity index 99% rename from crates/program/src/parser.rs rename to crates/clibc/src/parser.rs index 38f8cf5..843be18 100644 --- a/crates/program/src/parser.rs +++ b/crates/clibc/src/parser.rs @@ -1,5 +1,4 @@ //! Simple parser for reading typed values from a byte slice. -use core::convert::TryInto; use types::address::Address; use crate::vm_panic; diff --git a/crates/program/src/router.rs b/crates/clibc/src/router.rs similarity index 100% rename from crates/program/src/router.rs rename to crates/clibc/src/router.rs diff --git a/crates/program/src/storage.rs b/crates/clibc/src/storage.rs similarity index 78% rename from crates/program/src/storage.rs rename to crates/clibc/src/storage.rs index 6a78d18..78c6e74 100644 --- a/crates/program/src/storage.rs +++ b/crates/clibc/src/storage.rs @@ -1,15 +1,15 @@ -use types::O; +use types::{O, address::Address}; /// Domain constant for persistent storage pub const PERSISTENT_DOMAIN: &str = "P"; /// Trait for persistent structs pub trait Persistent { - fn load() -> O + fn load(address: &Address) -> O where Self: Sized; - fn store(&self); + fn store(&self, address: &Address); } /// Macro that defines persistent structs with embedded static key @@ -58,21 +58,22 @@ macro_rules! persist_struct { } } - pub fn load() -> $crate::types::O { - <$name as $crate::Persistent>::load() + pub fn load(address: &$crate::types::address::Address) -> $crate::types::O { + <$name as $crate::Persistent>::load(address) } - pub fn store(&self) { - <$name as $crate::Persistent>::store(self) + pub fn store(&self, address: &$crate::types::address::Address) { + <$name as $crate::Persistent>::store(self, address) } } impl $crate::Persistent for $name { - fn load() -> $crate::types::O { + fn load(address: &$crate::types::address::Address) -> $crate::types::O { #[cfg(target_arch = "riscv32")] unsafe { let key_ptr = $name::key_ptr(); let key_len = $name::key_len(); + let packed_lens: u32 = ((key_len as u32) << 16) | ($crate::PERSISTENT_DOMAIN.len() as u32); if key_len == 0 { $crate::vm_panic( @@ -84,10 +85,10 @@ macro_rules! persist_struct { core::arch::asm!( "li a7, 1", // syscall_storage_read "ecall", - in("a1") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant - in("a2") $crate::PERSISTENT_DOMAIN.len(), // domain len + in("a1") address.as_ref().as_ptr(), // address ptr + in("a2") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant in("a3") key_ptr, // key ptr - in("a4") key_len, // key len + in("a4") packed_lens, // packed lens (domain | key) out("a0") value_ptr, ); @@ -106,7 +107,7 @@ macro_rules! persist_struct { if value_len == 0 { $crate::require(value_len > 0, b"Decoded value len is 0 for bytes"); return $crate::types::O::None; - } + } let data_ptr = (value_ptr + 4) as *const u8; let value_buf = core::slice::from_raw_parts(data_ptr, value_len); @@ -116,16 +117,18 @@ macro_rules! persist_struct { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, return None $crate::types::O::None } } - fn store(&self) { + fn store(&self, address: &$crate::types::address::Address) { #[cfg(target_arch = "riscv32")] unsafe { let key_ptr = $name::key_ptr(); let key_len = $name::key_len(); + let packed_lens: u32 = ((key_len as u32) << 16) | ($crate::PERSISTENT_DOMAIN.len() as u32); if key_len == 0 { $crate::vm_panic( @@ -145,10 +148,10 @@ macro_rules! persist_struct { core::arch::asm!( "li a7, 2", // syscall_storage_write "ecall", - in("a1") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant - in("a2") $crate::PERSISTENT_DOMAIN.len(), // domain len + in("a1") address.as_ref().as_ptr(), // address ptr + in("a2") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant in("a3") key_ptr, // key ptr - in("a4") key_len, // key len + in("a4") packed_lens, // packed lens (domain | key) in("a5") val_ptr, // value ptr in("a6") val_len, // value len options(readonly, nostack, preserves_flags) @@ -157,6 +160,7 @@ macro_rules! persist_struct { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, do nothing } } diff --git a/crates/program/src/storage_map.rs b/crates/clibc/src/storage_map.rs similarity index 71% rename from crates/program/src/storage_map.rs rename to crates/clibc/src/storage_map.rs index 6f84259..f05e736 100644 --- a/crates/program/src/storage_map.rs +++ b/crates/clibc/src/storage_map.rs @@ -1,6 +1,5 @@ -use core::mem::{size_of, MaybeUninit}; use crate::{require, types::O, types::address::Address}; -use crate::logf; +use core::mem::{MaybeUninit, size_of}; /// Trait for types that can be used as storage keys in `StorageMap`. pub trait StorageKey { @@ -15,11 +14,10 @@ impl StorageKey for Address { } } - pub struct StorageMap; impl StorageMap { - pub fn get(domain: &[u8], key: &[u8]) -> O + pub fn get(address: &Address, domain: &[u8], key: &[u8]) -> O where V: Copy + Default, { @@ -31,14 +29,15 @@ impl StorageMap { #[cfg(target_arch = "riscv32")] unsafe { + let packed_lens: u32 = ((key.len() as u32) << 16) | (domain.len() as u32); let mut value_ptr: u32; core::arch::asm!( "li a7, 1", // syscall_storage_read "ecall", - in("a1") domain.as_ptr(), // a1 - domain ptr - in("a2") domain.len(), // a2 - domain len + in("a1") address.as_ref().as_ptr(), // a1 - address ptr + in("a2") domain.as_ptr(), // a2 - domain ptr in("a3") full_key.as_ptr(), // a3 - key ptr - in("a4") key.len(), // a4 - key len + in("a4") packed_lens, // a4 - packed lens (domain | key) out("a0") value_ptr, // a0 ); @@ -63,12 +62,13 @@ impl StorageMap { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, return None O::None } } - pub fn set(domain: &[u8], key: &[u8], val: V) + pub fn set(address: &Address, domain: &[u8], key: &[u8], val: V) where V: Copy, { @@ -78,19 +78,19 @@ impl StorageMap { let mut full_key = [0u8; 64]; full_key[..key.len()].copy_from_slice(key); - let val_bytes = unsafe { - core::slice::from_raw_parts((&val as *const V) as *const u8, size_of::()) - }; - + let val_bytes = + unsafe { core::slice::from_raw_parts((&val as *const V) as *const u8, size_of::()) }; + #[cfg(target_arch = "riscv32")] unsafe { + let packed_lens: u32 = ((key.len() as u32) << 16) | (domain.len() as u32); core::arch::asm!( "li a7, 2", // syscall_storage_write "ecall", - in("a1") domain.as_ptr(), // a1 - domain ptr - in("a2") domain.len(), // a2 - domain len + in("a1") address.as_ref().as_ptr(), // a1 - address ptr + in("a2") domain.as_ptr(), // a2 - domain ptr in("a3") full_key.as_ptr(), // a3 - key ptr - in("a4") key.len(), // a4 - key len + in("a4") packed_lens, // a4 - packed lens (domain | key) in("a5") val_bytes.as_ptr(), // a5 - value ptr in("a6") val_bytes.len(), // a6 - value len options(readonly, nostack, preserves_flags) @@ -99,12 +99,12 @@ impl StorageMap { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, do nothing } } } - #[macro_export] macro_rules! Map { ($name:ident) => { @@ -131,30 +131,37 @@ macro_rules! Map { key_len } - pub fn get(key: K) -> $crate::types::O + pub fn get( + address: &$crate::types::address::Address, + key: K, + ) -> $crate::types::O where K: $crate::StorageKey, V: Copy + Default, { let mut buf = [0u8; Self::MAX_KEY_LEN]; let total_len = Self::build_key(key, &mut buf); - $crate::StorageMap::get::(Self::DOMAIN_NAME.as_bytes(), &buf[..total_len]) + $crate::StorageMap::get::( + address, + Self::DOMAIN_NAME.as_bytes(), + &buf[..total_len], + ) } - pub fn set(key: K, val: V) + pub fn set(address: &$crate::types::address::Address, key: K, val: V) where K: $crate::StorageKey, V: Copy, { let mut buf = [0u8; Self::MAX_KEY_LEN]; let total_len = Self::build_key(key, &mut buf); - $crate::StorageMap::set::(Self::DOMAIN_NAME.as_bytes(), &buf[..total_len], val); + $crate::StorageMap::set::( + address, + Self::DOMAIN_NAME.as_bytes(), + &buf[..total_len], + val, + ); } } }; } - - - - - diff --git a/crates/clibc/src/syscalls.rs b/crates/clibc/src/syscalls.rs new file mode 100644 index 0000000..73e6167 --- /dev/null +++ b/crates/clibc/src/syscalls.rs @@ -0,0 +1,11 @@ +/// System call IDs shared between the guest program ABI and the runtime. +pub const SYSCALL_STORAGE_GET: u32 = 1; +pub const SYSCALL_STORAGE_SET: u32 = 2; +pub const SYSCALL_PANIC: u32 = 3; +pub const SYSCALL_CALL_PROGRAM: u32 = 5; +pub const SYSCALL_FIRE_EVENT: u32 = 6; +pub const SYSCALL_ALLOC: u32 = 7; +pub const SYSCALL_DEALLOC: u32 = 8; +pub const SYSCALL_TRANSFER: u32 = 9; +pub const SYSCALL_BALANCE: u32 = 10; +pub const SYSCALL_BRK: u32 = 214; // brk(2): set program break (heap end) diff --git a/crates/program/src/transfer.rs b/crates/clibc/src/transfer.rs similarity index 100% rename from crates/program/src/transfer.rs rename to crates/clibc/src/transfer.rs diff --git a/crates/program/tests/router.rs b/crates/clibc/tests/router.rs similarity index 97% rename from crates/program/tests/router.rs rename to crates/clibc/tests/router.rs index fa00842..5f9a5a3 100644 --- a/crates/program/tests/router.rs +++ b/crates/clibc/tests/router.rs @@ -1,4 +1,4 @@ -use program::router::{decode_calls, route, FuncCall}; +use clibc::router::{decode_calls, route, FuncCall}; use types::{Result, Address}; #[test] diff --git a/crates/compiler/src/abi_codegen.rs b/crates/compiler/src/abi_codegen.rs index d7c0c3f..62725ba 100644 --- a/crates/compiler/src/abi_codegen.rs +++ b/crates/compiler/src/abi_codegen.rs @@ -27,9 +27,9 @@ impl AbiCodeGenerator { // Don't add imports - assume they're in the parent file code.push_str("// Note: This code assumes the following imports in the parent file:\n"); - code.push_str("// use program::types::address::Address;\n"); - code.push_str("// use program::types::result::Result;\n"); - code.push_str("// use program::call::call;\n\n"); + code.push_str("// use clibc::types::address::Address;\n"); + code.push_str("// use clibc::types::result::Result;\n"); + code.push_str("// use clibc::call::call;\n\n"); // Generate contract struct code.push_str(&format!("/// Client for interacting with {} contract\n", self.contract_name)); diff --git a/crates/compiler/src/elf.rs b/crates/compiler/src/elf.rs index 23af11d..454f792 100644 --- a/crates/compiler/src/elf.rs +++ b/crates/compiler/src/elf.rs @@ -1,4 +1,5 @@ use goblin::elf::Elf; +use goblin::elf::section_header::SHT_NOBITS; pub struct ElfInfo<'a> { pub code: &'a [u8], @@ -68,6 +69,29 @@ impl<'a> ElfInfo<'a> { pub fn get_section_by_name(&self, name: &str) -> Option<&ElfSection<'a>> { self.sections.iter().find(|s| s.name == name) } + + /// Returns a flat `.bss` range (length is zeroed by loader), and base address. + pub fn get_flat_bss(&self) -> Option<(Vec, u64)> { + let bss_sections: Vec<&ElfSection> = self + .sections + .iter() + .filter(|s| s.name.starts_with(".bss") || s.name.starts_with(".sbss")) + .collect(); + + if bss_sections.is_empty() { + return None; + } + + let min_addr = bss_sections.iter().map(|s| s.addr).min().unwrap(); + let max_addr = bss_sections + .iter() + .map(|s| s.addr + s.size) + .max() + .unwrap(); + + let total_size = (max_addr - min_addr) as usize; + Some((vec![0u8; total_size], min_addr)) + } } @@ -80,8 +104,13 @@ pub fn parse_elf_from_bytes<'a>(bytes: &'a [u8]) -> Result, goblin:: let offset = section.sh_offset as usize; let size = section.sh_size as usize; - if offset + size <= bytes.len() { - let data = &bytes[offset..offset + size]; + let is_nobits = section.sh_type == SHT_NOBITS; + if offset + size <= bytes.len() || is_nobits { + let data = if is_nobits { + &bytes[0..0] + } else { + &bytes[offset..offset + size] + }; sections.push(ElfSection { name: name.to_string(), addr: section.sh_addr, diff --git a/crates/compiler/tests/abi_generator_tests.rs b/crates/compiler/tests/abi_generator_tests.rs index 61d1227..97affae 100644 --- a/crates/compiler/tests/abi_generator_tests.rs +++ b/crates/compiler/tests/abi_generator_tests.rs @@ -839,6 +839,24 @@ fn test_erc20_example_file_generates_typed_abi() { assert_eq!(transfer_func.inputs[1].name, "amount"); assert!(matches!(transfer_func.inputs[1].kind, ParamType::Uint(32))); + let approve_func = abi.functions.iter().find(|f| f.name == "approve").unwrap(); + assert_eq!(approve_func.selector, 3); + assert_eq!(approve_func.inputs.len(), 2); + assert_eq!(approve_func.inputs[0].name, "spender"); + assert!(matches!(approve_func.inputs[0].kind, ParamType::Address)); + assert_eq!(approve_func.inputs[1].name, "amount"); + assert!(matches!(approve_func.inputs[1].kind, ParamType::Uint(32))); + + let transfer_from = abi.functions.iter().find(|f| f.name == "transfer_from").unwrap(); + assert_eq!(transfer_from.selector, 4); + assert_eq!(transfer_from.inputs.len(), 3); + assert_eq!(transfer_from.inputs[0].name, "from"); + assert!(matches!(transfer_from.inputs[0].kind, ParamType::Address)); + assert_eq!(transfer_from.inputs[1].name, "to"); + assert!(matches!(transfer_from.inputs[1].kind, ParamType::Address)); + assert_eq!(transfer_from.inputs[2].name, "amount"); + assert!(matches!(transfer_from.inputs[2].kind, ParamType::Uint(32))); + let balance_func = abi.functions.iter().find(|f| f.name == "balance_of").unwrap(); assert_eq!(balance_func.selector, 5); assert_eq!(balance_func.inputs.len(), 1); diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 707c6e8..fed7b87 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -4,18 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -program = { path = "../program" } +clibc = { path = "../clibc", features = ["guest"] } sha2 = { version = "0.10", default-features = false } k256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdsa", "alloc"] } -[dev-dependencies] -types = { path = "../types" } # adjust path as needed -compiler = { path = "../compiler" } # adjust path as needed -avm = { path = "../avm" } # adjust path as needed -state = { path = "../state" } -once_cell = "1.19.0" -serde_json = "1.0" - [features] default = [] binaries = [] # Enable this feature to build RISC-V binaries diff --git a/crates/examples/README.md b/crates/examples/README.md index 640b842..4f6d807 100644 --- a/crates/examples/README.md +++ b/crates/examples/README.md @@ -58,6 +58,30 @@ Demonstrates how one contract can call another contract. - **Features**: External contract calls, result handling - **Use cases**: DeFi protocols, modular contract systems +### 8. **logging.rs** - Logging and Tracing +Emits structured logs from inside the VM. +- **Purpose**: Show runtime logging patterns +- **Features**: Kernel log output, string formatting +- **Use cases**: Debugging contract behavior + +### 9. **native_transfer.rs** - Native Transfer Syscall +Uses the transfer syscall from a guest program. +- **Purpose**: Demonstrate native token transfers +- **Features**: Syscall usage, parameter encoding +- **Use cases**: Simple payments, account funding + +### 10. **dex.rs** - Simple AMM +Implements a basic AMM between native AM and an ERC-20 token. +- **Purpose**: Show multi-contract interactions and liquidity flows +- **Features**: Add/remove liquidity, swaps +- **Use cases**: DeFi primitives + +### 11. **ecdsa_verify.rs** - Signature Verification +Verifies an ECDSA signature inside the VM. +- **Purpose**: Demonstrate cryptographic verification +- **Features**: Signature parsing, hashing, verification +- **Use cases**: Auth, permit-style flows + ## Project Structure ``` @@ -67,7 +91,6 @@ examples/ β”‚ β”œβ”€β”€ *.elf # RISC-V ELF binaries β”‚ β”œβ”€β”€ *.abi.json # Contract ABI definitions β”‚ └── *_client.rs # Auto-generated client code -β”œβ”€β”€ tests/ # Integration tests β”œβ”€β”€ Makefile # Build system β”œβ”€β”€ Cargo.toml # Rust dependencies └── generate_abis.sh # ABI generation script @@ -75,9 +98,9 @@ examples/ ## Running Tests -To run the integration tests for all examples: +Example integration tests run through the `aTester` crate in the workspace: ```bash -cargo test +make run_examples ``` ## Key Concepts Demonstrated diff --git a/crates/examples/src/allocator_demo.rs b/crates/examples/src/allocator_demo.rs index 508f275..78ab601 100644 --- a/crates/examples/src/allocator_demo.rs +++ b/crates/examples/src/allocator_demo.rs @@ -3,14 +3,17 @@ extern crate alloc; -use program::{entrypoint, types::result::Result, types::address::Address, require, vm_panic, DataParser}; +use clibc::{ + DataParser, entrypoint, require, types::address::Address, types::result::Result, vm_panic, +}; /// Guest program that demonstrates heap allocation using VM syscalls entrypoint!(main); -fn main(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn main(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Need to import alloc types after entrypoint macro includes the allocator - use alloc::vec::Vec; use alloc::collections::BTreeMap; + use alloc::vec::Vec; // Expect at least 6 u32 values in little-endian form: // - first 3 populate the Vec diff --git a/crates/examples/src/call_program.rs b/crates/examples/src/call_program.rs index f40450a..9dfb935 100644 --- a/crates/examples/src/call_program.rs +++ b/crates/examples/src/call_program.rs @@ -1,11 +1,11 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::{entrypoint, types::result::Result, require, vm_panic, DataParser}; -use program::call::call; -use program::types::address::Address; +use clibc::call::call; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result, vm_panic}; // Include the auto-generated ABI client code for simple program include!("../bin/simple_abi.rs"); @@ -16,7 +16,8 @@ include!("../bin/simple_abi.rs"); /// The program expects: /// - 20 bytes: Address of the simple contract /// - 8 bytes: Two u32 values to compare (4 bytes each) -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, caller: Address, data: &[u8]) -> Result { + let _ = program; // Ensure there's enough data require(data.len() == 28, b"input data must be 28 bytes"); @@ -37,7 +38,7 @@ fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result call_data[4..8].copy_from_slice(&second.to_le_bytes()); // Call the simple contract using the generated client's call_main method - let ret = match simple_client.call_main(&_caller, &call_data) { + let ret = match simple_client.call_main(&caller, &call_data) { Some(result) => result, None => vm_panic(b"program call failed"), }; diff --git a/crates/examples/src/dex.rs b/crates/examples/src/dex.rs index 93c8d1d..614a393 100644 --- a/crates/examples/src/dex.rs +++ b/crates/examples/src/dex.rs @@ -1,14 +1,14 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::{ +use clibc::{ + DataParser, Map, call::call, - entrypoint, event, fire_event, persist_struct, DataParser, require, vm_panic, transfer, - hex_address, + entrypoint, event, fire_event, hex_address, persist_struct, require, transfer, types::{address::Address, o::O, result::Result}, - Map, + vm_panic, }; // Generated ABI client for ERC20 (included like in call_program example) @@ -52,8 +52,8 @@ const ADD_LIQUIDITY: u8 = 0x01; const REMOVE_LIQUIDITY: u8 = 0x02; const SWAP: u8 = 0x03; -fn load_pool() -> Pool { - match Pool::load() { +fn load_pool(program: &Address) -> Pool { + match Pool::load(program) { O::Some(p) => p, O::None => Pool { reserve_am: 0, @@ -63,14 +63,14 @@ fn load_pool() -> Pool { } } -fn get_liquidity(owner: Address) -> u128 { - match Liquidity::get(owner) { +fn get_liquidity(program: &Address, owner: Address) -> u128 { + match Liquidity::get(program, owner) { O::Some(v) => v, O::None => 0, } } -fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn add_liquidity(program: Address, caller: Address, mut parser: DataParser) -> Result { // Adds liquidity by pulling both legs (native AM and ERC20) from the caller, // mints LP shares proportional to the existing reserves, and emits an event. let erc20 = Erc20Contract::new(erc20_address()); @@ -82,16 +82,16 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> require(token_in <= u32::MAX as u128, b"add: token overflow"); // Collect AM from caller into the pool address (native balance increases). - require(transfer!(&self_addr, am_in), b"add: am transfer failed"); + require(transfer!(&program, am_in), b"add: am transfer failed"); // Pull ERC20 from caller into the pool address. let ok = erc20 - .transfer(&caller, self_addr, token_in as u32) + .transfer_from(&program, caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"add: token transfer failed"); - let mut pool = load_pool(); + let mut pool = load_pool(&program); let minted = if pool.total_liquidity == 0 { am_in as u128 @@ -107,10 +107,10 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> pool.reserve_am = pool.reserve_am.saturating_add(am_in as u128); pool.reserve_token = pool.reserve_token.saturating_add(token_in); pool.total_liquidity = pool.total_liquidity.saturating_add(minted); - pool.store(); + pool.store(&program); - let user_liq = get_liquidity(caller).saturating_add(minted); - Liquidity::set(caller, user_liq); + let user_liq = get_liquidity(&program, caller).saturating_add(minted); + Liquidity::set(&program, caller, user_liq); if token_in <= u64::MAX as u128 { fire_event!(LiquidityAdded::new(caller, am_in, token_in as u64)); @@ -121,15 +121,15 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> res } -fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn remove_liquidity(program: Address, caller: Address, mut parser: DataParser) -> Result { // Burns LP shares for AM + ERC20 payouts, updates reserves, and emits LiquidityRemoved. let erc20 = Erc20Contract::new(erc20_address()); require(parser.remaining() >= 8, b"remove: missing args"); let shares = parser.read_u64() as u128; require(shares > 0, b"remove: zero shares"); - let mut pool = load_pool(); - let user_shares = get_liquidity(caller); + let mut pool = load_pool(&program); + let user_shares = get_liquidity(&program, caller); require(user_shares >= shares, b"remove: not enough shares"); require(pool.total_liquidity > 0, b"remove: empty pool"); @@ -141,23 +141,30 @@ fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) pool.reserve_am = pool.reserve_am.saturating_sub(am_out); pool.reserve_token = pool.reserve_token.saturating_sub(token_out); pool.total_liquidity = pool.total_liquidity.saturating_sub(shares); - pool.store(); + pool.store(&program); - Liquidity::set(caller, user_shares - shares); + Liquidity::set(&program, caller, user_shares - shares); // Pay out ERC20 tokens from pool balance. require(token_out <= u32::MAX as u128, b"remove: token overflow"); let ok = erc20 - .transfer(&self_addr, caller, token_out as u32) + .transfer(&program, caller, token_out as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"remove: token transfer failed"); // Pay native AM out to the provider. Note: with the current host interface, // native transfers debit the caller context. - require(transfer!(&caller, am_out as u64), b"remove: am transfer failed"); + require( + transfer!(&caller, am_out as u64), + b"remove: am transfer failed", + ); - fire_event!(LiquidityRemoved::new(caller, am_out as u64, token_out as u64)); + fire_event!(LiquidityRemoved::new( + caller, + am_out as u64, + token_out as u64 + )); // AM payouts are reported in the result for visibility. let mut res = Result::new(true, 0); @@ -168,7 +175,7 @@ fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) res } -fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn swap(program: Address, caller: Address, mut parser: DataParser) -> Result { // Constant-product swap. Direction 0 = AM -> ERC20, Direction 1 = ERC20 -> AM. let erc20 = Erc20Contract::new(erc20_address()); require(parser.remaining() >= 9, b"swap: missing args"); @@ -176,26 +183,32 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { let amount = parser.read_u64(); require(amount > 0, b"swap: zero amount"); - let mut pool = load_pool(); - require(pool.reserve_am > 0 && pool.reserve_token > 0, b"swap: empty pool"); + let mut pool = load_pool(&program); + require( + pool.reserve_am > 0 && pool.reserve_token > 0, + b"swap: empty pool", + ); if direction == 0 { let am_in = amount; // Collect AM into the pool. - let ok = transfer!(&self_addr, am_in); + let ok = transfer!(&program, am_in); require(ok, b"swap: am transfer failed"); let token_out = (am_in as u128 * pool.reserve_token) / (pool.reserve_am + am_in as u128); require(token_out > 0, b"swap: zero output"); - require(token_out <= pool.reserve_token, b"swap: insufficient tokens"); + require( + token_out <= pool.reserve_token, + b"swap: insufficient tokens", + ); require(token_out <= u32::MAX as u128, b"swap: token overflow"); pool.reserve_am = pool.reserve_am.saturating_add(am_in as u128); pool.reserve_token = pool.reserve_token.saturating_sub(token_out); - pool.store(); + pool.store(&program); let ok = erc20 - .transfer(&self_addr, caller, token_out as u32) + .transfer(&program, caller, token_out as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"swap: token transfer failed"); @@ -212,7 +225,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { // Pull ERC20 into the pool. let ok = erc20 - .transfer(&caller, self_addr, token_in as u32) + .transfer_from(&program, caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"swap: token transfer failed"); @@ -224,7 +237,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { pool.reserve_token = pool.reserve_token.saturating_add(token_in); pool.reserve_am = pool.reserve_am.saturating_sub(am_out); - pool.store(); + pool.store(&program); // Pay native AM to the trader and leave ERC20 in the pool. require(transfer!(&caller, am_out as u64), b"swap: am payout failed"); @@ -237,7 +250,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { } } -fn dex_entry(self_addr: Address, caller: Address, data: &[u8]) -> Result { +fn dex_entry(program: Address, caller: Address, data: &[u8]) -> Result { // Simple selector-based router: first byte is op, remainder is args for the op handlers. if data.is_empty() { vm_panic(b"missing selector"); @@ -247,9 +260,9 @@ fn dex_entry(self_addr: Address, caller: Address, data: &[u8]) -> Result { let op = parser.read_bytes(1)[0]; match op { - ADD_LIQUIDITY => add_liquidity(self_addr, caller, parser), - REMOVE_LIQUIDITY => remove_liquidity(self_addr, caller, parser), - SWAP => swap(self_addr, caller, parser), + ADD_LIQUIDITY => add_liquidity(program, caller, parser), + REMOVE_LIQUIDITY => remove_liquidity(program, caller, parser), + SWAP => swap(program, caller, parser), _ => vm_panic(b"unknown selector"), } } diff --git a/crates/examples/src/ecdsa_verify.rs b/crates/examples/src/ecdsa_verify.rs index f5632a2..00b044e 100644 --- a/crates/examples/src/ecdsa_verify.rs +++ b/crates/examples/src/ecdsa_verify.rs @@ -1,11 +1,11 @@ #![no_std] #![no_main] -extern crate program; -use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; -use program::{ - entrypoint, log, logf, require, types::address::Address, types::result::Result, vm_panic, - DataParser, HexCodec, +extern crate clibc; +use k256::ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier}; +use clibc::{ + DataParser, HexCodec, entrypoint, log, logf, require, types::address::Address, + types::result::Result, vm_panic, }; /// ECDSA verification example using k256. @@ -14,17 +14,23 @@ use program::{ /// - N bytes: SEC1-encoded pubkey /// - 64 bytes: signature (r||s) /// - 32 bytes: message hash (already hashed) -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; let mut parser = DataParser::new(data); let pk_len = parser.read_bytes(1)[0] as usize; - require(pk_len == 33 || pk_len == 65, b"pubkey must be 33 or 65 bytes"); + require( + pk_len == 33 || pk_len == 65, + b"pubkey must be 33 or 65 bytes", + ); let pk_bytes = parser.read_bytes(pk_len); let sig_bytes = parser.read_bytes(64); let hash = parser.read_bytes(32); - let verifying_key = VerifyingKey::from_sec1_bytes(pk_bytes).unwrap_or_else(|_| vm_panic(b"invalid pubkey")); - let signature = Signature::from_slice(sig_bytes).unwrap_or_else(|_| vm_panic(b"invalid signature")); + let verifying_key = + VerifyingKey::from_sec1_bytes(pk_bytes).unwrap_or_else(|_| vm_panic(b"invalid pubkey")); + let signature = + Signature::from_slice(sig_bytes).unwrap_or_else(|_| vm_panic(b"invalid signature")); // Log the received inputs in hex for visibility logf!("ecdsa_verify: pk_len=%d", pk_len as u32); diff --git a/crates/examples/src/erc20.rs b/crates/examples/src/erc20.rs index 2d37136..c63ce68 100644 --- a/crates/examples/src/erc20.rs +++ b/crates/examples/src/erc20.rs @@ -1,12 +1,11 @@ #![no_std] #![no_main] -extern crate program; -use program::{entrypoint, event, - fire_event, log, logf, persist_struct, - require, router::route, DataParser, - types::{address::Address, o::O, result::Result}, vm_panic, Map}; - +extern crate clibc; +use clibc::{ + DataParser, Map, StorageKey, entrypoint, event, fire_event, log, logf, persist_struct, + require, router::route, types::{address::Address, o::O, result::Result}, vm_panic, +}; // Persistent structs persist_struct!(Metadata { @@ -26,38 +25,77 @@ event!(Transfer { }); Map!(Balances); +Map!(Allowances); -unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { - route(data, program, caller, - |to, from, call| match call.selector { - 0x01 => { - init(caller, call.args); - Result::new(true, 0) - }, - 0x02 => { - let mut parser = DataParser::new(call.args); - let to = parser.read_address(); - let amount = parser.read_u32(); - transfer(caller, to, amount); - Result::new(true, 0) - }, - 0x05 => { - let mut parser = DataParser::new(call.args); - let owner = parser.read_address(); - let b = balance_of(owner); - Result::with_u32(b) - }, - _ => vm_panic(b"unknown selector"), +struct AllowanceKey { + bytes: [u8; 40], +} + +impl AllowanceKey { + fn new(owner: Address, spender: Address) -> Self { + let mut bytes = [0u8; 40]; + bytes[..20].copy_from_slice(&owner.0); + bytes[20..].copy_from_slice(&spender.0); + Self { bytes } + } +} + +impl StorageKey for AllowanceKey { + fn as_storage_key(&self) -> &[u8] { + &self.bytes + } +} + +unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { + route(data, program, caller, |to, from, call| { + match call.selector { + 0x01 => { + init(&program, caller, call.args); + Result::new(true, 0) + } + 0x02 => { + let mut parser = DataParser::new(call.args); + let to = parser.read_address(); + let amount = parser.read_u32(); + transfer(&program, caller, to, amount); + Result::new(true, 0) + } + 0x03 => { + let mut parser = DataParser::new(call.args); + let spender = parser.read_address(); + let amount = parser.read_u32(); + approve(&program, caller, spender, amount); + Result::new(true, 0) + } + 0x04 => { + let mut parser = DataParser::new(call.args); + let from = parser.read_address(); + let to = parser.read_address(); + let amount = parser.read_u32(); + transfer_from(&program, caller, from, to, amount); + Result::new(true, 0) + } + 0x05 => { + let mut parser = DataParser::new(call.args); + let owner = parser.read_address(); + let b = balance_of(&program, owner); + Result::with_u32(b) + } + _ => vm_panic(b"unknown selector"), + } }) } -fn init(caller: Address, args: &[u8]) { +fn init(program: &Address, caller: Address, args: &[u8]) { logf!("init called"); - let mut meta = match Metadata::load() { + let mut meta = match Metadata::load(program) { O::Some(m) => vm_panic(b"already initialized"), - O::None => Metadata { total_supply: 0, decimals: 0 }, + O::None => Metadata { + total_supply: 0, + decimals: 0, + }, }; - + logf!("initializing"); let mut parser = DataParser::new(args); @@ -69,41 +107,80 @@ fn init(caller: Address, args: &[u8]) { meta.total_supply = total_supply; meta.decimals = decimals; - meta.store(); + meta.store(program); // mint to caller - mint(caller, total_supply); + mint(program, caller, total_supply); } -fn mint(caller: Address, val: u32) { +fn mint(program: &Address, caller: Address, val: u32) { logf!("minting: %d tokens", val); fire_event!(Minted::new(caller, val)); - Balances::set(caller, val); + Balances::set(program, caller, val); } -fn transfer(caller: Address, to: Address, amount: u32) { - let from_bal = match Balances::get(caller) { +fn transfer(program: &Address, caller: Address, to: Address, amount: u32) { + logf!("erc20: transfer amount=%d", amount); + let from_bal = match Balances::get(program, caller) { O::Some(bal) => bal, O::None => 0, }; - + if from_bal < amount { vm_panic(b"insufficient"); } - let to_bal = match Balances::get(to) { + let to_bal = match Balances::get(program, to) { O::Some(bal) => bal, O::None => 0, }; - - Balances::set(caller, from_bal - amount); - Balances::set(to, to_bal + amount); - + + Balances::set(program, caller, from_bal - amount); + Balances::set(program, to, to_bal + amount); + fire_event!(Transfer::new(caller, to, amount)); } -fn balance_of(owner: Address) -> u32 { - match Balances::get(owner) { +fn approve(program: &Address, caller: Address, spender: Address, amount: u32) { + let key = AllowanceKey::new(caller, spender); + Allowances::set(program, key, amount); +} + +fn transfer_from( + program: &Address, + caller: Address, + from: Address, + to: Address, + amount: u32, +) { + let allowance = match Allowances::get(program, AllowanceKey::new(from, caller)) { + O::Some(val) => val, + O::None => 0, + }; + require(allowance >= amount, b"allowance insufficient"); + + let from_bal = match Balances::get(program, from) { + O::Some(bal) => bal, + O::None => 0, + }; + if from_bal < amount { + vm_panic(b"insufficient"); + } + + let to_bal = match Balances::get(program, to) { + O::Some(bal) => bal, + O::None => 0, + }; + + Allowances::set(program, AllowanceKey::new(from, caller), allowance - amount); + Balances::set(program, from, from_bal - amount); + Balances::set(program, to, to_bal + amount); + + fire_event!(Transfer::new(from, to, amount)); +} + +fn balance_of(program: &Address, owner: Address) -> u32 { + match Balances::get(program, owner) { O::Some(bal) => bal, O::None => 0, } diff --git a/crates/examples/src/lib_import.rs b/crates/examples/src/lib_import.rs index 8ad3727..1a9add2 100644 --- a/crates/examples/src/lib_import.rs +++ b/crates/examples/src/lib_import.rs @@ -1,58 +1,59 @@ #![no_std] #![no_main] -extern crate program; -use program::{entrypoint, types::result::Result, require}; -use program::types::address::Address; +extern crate clibc; +use clibc::types::address::Address; +use clibc::{entrypoint, require, types::result::Result}; // Import the sha2 library for hashing -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; /// Example program that imports and uses an external library (sha2) -/// +/// /// This demonstrates importing and using an external cryptographic library /// within a smart contract environment. -/// +/// /// CONTRACT BEHAVIOR: /// - Takes arbitrary input data /// - Computes SHA-256 hash of the input /// - Returns the 32-byte hash -/// +/// /// INPUT FORMAT: Any arbitrary bytes -/// +/// /// OUTPUT FORMAT: Returns a Result struct with: /// - success: true (always succeeds if input is valid) /// - error_code: 0 (no error) /// - data_len: 32 (size of SHA-256 hash) /// - data: The SHA-256 hash as 32 bytes -/// +/// /// REAL-WORLD USAGE: /// - Data integrity verification /// - Creating commitments for reveal schemes /// - Generating deterministic IDs from data /// - Proof of data existence at a point in time -fn hasher_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn hasher_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Validate that we have some input data require(data.len() > 0, b"Input data cannot be empty"); - + // Create a new SHA-256 hasher instance let mut hasher = Sha256::new(); - + // Feed the input data to the hasher hasher.update(data); - + // Compute the hash and get the result as a fixed array let hash_result = hasher.finalize(); - + // Create a result with the hash data let mut result = Result::new(true, 0); - + // Copy the 32-byte hash into the result's data field result.data[..32].copy_from_slice(&hash_result[..]); result.data_len = 32; - + result } // Register the function as the entrypoint -entrypoint!(hasher_entry); \ No newline at end of file +entrypoint!(hasher_entry); diff --git a/crates/examples/src/logging.rs b/crates/examples/src/logging.rs index 57f6996..7aced04 100644 --- a/crates/examples/src/logging.rs +++ b/crates/examples/src/logging.rs @@ -1,63 +1,63 @@ #![no_std] #![no_main] -extern crate program; -use program::{entrypoint, types::result::Result, logf, log, concat_str, DataParser}; -use program::types::address::Address; +extern crate clibc; use core::fmt; +use clibc::types::address::Address; +use clibc::{DataParser, concat_str, entrypoint, log, logf, types::result::Result}; /// Comprehensive logging demonstration showing all format specifiers -unsafe fn logging(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +unsafe fn logging(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Simple string logging logf!("=== Logging Demo Started ==="); - + // Integer formats let num = 42; logf!("Decimal: %d", num); logf!("Unsigned: %u", num); logf!("Hexadecimal: %x", 0xDEADBEEF); - + // Multiple values in one log let x = 10; let y = 20; logf!("x=%d, y=%d, sum=%d", x, y, x + y); - + // Character logging let ch = 'A' as u32; logf!("Character: %c", ch); - + // Floating point let pi_bits = 3.14159f32.to_bits(); logf!("Pi approximation: %f", pi_bits); - + // String logging - now simplified! let msg = b"Hello, VM!"; log!("Message: %s", msg); - + // String concatenation - requires a buffer in no_std (even though we have an allocator) - let mut buffer = [0u8; 64]; // Stack-allocated storage + let mut buffer = [0u8; 64]; // Stack-allocated storage let greeting = concat_str!(buffer, b"Hello, ", b"World", b"!"); log!("Concatenated: %s", greeting); - - + // Byte array logging (hex format) - simplified! let bytes = [0xDE, 0xAD, 0xBE, 0xEF]; log!("Bytes (hex): %b", bytes); - + // Array of u32s - simplified! let numbers = [1u32, 2, 3, 4, 5]; log!("Numbers: %a", numbers); - + // Array of u8s (decimal format) - simplified! let bytes_decimal = [10u8, 20, 30, 40, 50]; log!("Bytes (decimal): %A", bytes_decimal); - + // Process input data let mut parser = DataParser::new(data); if parser.remaining() >= 4 { let value = parser.read_u32(); logf!("Input value: %d (0x%x)", value, value); - + // Log remaining bytes if any if parser.remaining() > 0 { let remaining = parser.read_bytes(parser.remaining()); @@ -67,52 +67,56 @@ unsafe fn logging(_self_address: Address, _caller: Address, data: &[u8]) -> Resu } else { logf!("Input too short: %d bytes", data.len() as u32); } - + // Demonstrate escape sequence logf!("100%% complete!"); - + // Complex example with mixed types let score = 95; let grade = 'A' as u32; let bonus = 5; - logf!("Score: %d + Bonus: %d = Total: %d", - score, bonus, score + bonus); + logf!( + "Score: %d + Bonus: %d = Total: %d", + score, + bonus, + score + bonus + ); logf!("Grade: %c", grade); - + // Large array (partial display for efficiency) - simplified! let large_array = [1u32, 2, 3, 4, 5, 6, 7, 8, 9, 10]; log!("Large array: %a", large_array); - + // Debug and Display trait demonstrations logf!("=== Debug and Display Trait Logging ==="); - + // Create a custom struct that implements Debug and Display let point = Point { x: 10, y: 20 }; logf!("Point (Debug): %s", debug: point); logf!("Point (Display): %s", display: point); - + // Test with Option types (implements Debug) let some_value: Option = Some(42); let none_value: Option = None; logf!("Some value: %s", debug: some_value); logf!("None value: %s", debug: none_value); - + // Test with Result types (implements Debug) let ok_result: core::result::Result = Ok(100); let err_result: core::result::Result = Err("error message"); logf!("Ok result: %s", debug: ok_result); logf!("Err result: %s", debug: err_result); - + // Test with arrays (Debug) let debug_array = [1, 2, 3, 4, 5]; logf!("Array debug: %s", debug: debug_array); - + // Test with tuples (Debug) let tuple = (42, "hello", true); logf!("Tuple: %s", debug: tuple); - + logf!("=== Logging Demo Complete ==="); - + Result::new(true, 0) } diff --git a/crates/examples/src/multi_func.rs b/crates/examples/src/multi_func.rs index 3f214c1..4d8fe4f 100644 --- a/crates/examples/src/multi_func.rs +++ b/crates/examples/src/multi_func.rs @@ -1,52 +1,54 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::{entrypoint, types::result::Result, vm_panic, require, DataParser}; -use program::types::address::Address; -use program::router::{route}; +use clibc::router::route; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result, vm_panic}; /// Main entry point for the smart contract. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates a multi-function smart contract /// that can handle different operations based on a selector. This is a common /// pattern in smart contract development, similar to how web APIs work. -/// +/// /// FUNCTION ROUTING: The contract uses a selector (function ID) to determine /// which function to call. This allows one contract to provide multiple /// different operations. -/// +/// /// PARAMETERS: -/// - _self_address: The address of this contract (unused in this example) +/// - program: The address of this contract (unused in this example) /// - _caller: The address calling this contract (unused in this example) /// - data: Binary data containing the function selector and arguments -/// +/// /// RETURN VALUE: A Result indicating success/failure and any return data -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { // EDUCATIONAL: Use the router to handle multiple function calls // The router decodes the input data and calls the appropriate function - route(data, _self_address, _caller, |to, from,call| match call.selector { - 0x01 => compare(call.args), // Function selector 0x01 = compare function - 0x02 => other(call.args), // Function selector 0x02 = other function - _ => vm_panic(b"unknown selector"), // Unknown selector = panic + route(data, program, _caller, |to, from, call| { + match call.selector { + 0x01 => compare(call.args), // Function selector 0x01 = compare function + 0x02 => other(call.args), // Function selector 0x02 = other function + _ => vm_panic(b"unknown selector"), // Unknown selector = panic + } }) } /// Compares two 32-bit integers and returns the larger one. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates how to handle binary data in smart /// contracts. The function receives raw bytes and must parse them into /// meaningful data structures. -/// +/// /// INPUT FORMAT: 8 bytes total /// - First 4 bytes: First integer (little-endian) /// - Last 4 bytes: Second integer (little-endian) -/// +/// /// RETURN LOGIC: /// - If first number > second number: success = true, error_code = first number /// - If first number <= second number: success = false, error_code = second number -/// +/// /// EDUCATIONAL NOTE: The return value uses the Result struct's fields in a /// non-standard way - error_code actually contains the larger number. This /// is just for demonstration purposes. @@ -71,11 +73,11 @@ fn compare(data: &[u8]) -> Result { } /// Example function that always fails. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates error handling in smart contracts. /// Sometimes functions need to fail intentionally (e.g., when conditions /// aren't met or for testing purposes). -/// +/// /// USAGE: This function is called when selector 0x02 is used. It always /// panics with the message "Intentional failure", which will cause the /// entire transaction to fail and revert any state changes. diff --git a/crates/examples/src/native_transfer.rs b/crates/examples/src/native_transfer.rs index 66a790b..d9bf350 100644 --- a/crates/examples/src/native_transfer.rs +++ b/crates/examples/src/native_transfer.rs @@ -1,17 +1,18 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::{entrypoint, require, DataParser}; -use program::types::address::Address; -use program::types::result::Result; +use clibc::types::address::Address; +use clibc::types::result::Result; +use clibc::{DataParser, entrypoint, require}; /// Demonstrates transferring the native AM token from the caller to a target /// address using the VM's transfer syscall. The input payload is: /// - 20 bytes: destination address /// - 8 bytes: amount (little-endian u64) -fn transfer_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn transfer_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; let mut parser = DataParser::new(data); // Need at least 20 bytes for the address and 8 bytes for the value require(parser.remaining() >= 28, b"transfer: need addr + amount"); @@ -20,9 +21,9 @@ fn transfer_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Resu let amount = parser.read_u64(); // Capture recipient balance before/after for return value - let _before = program::balance!(&to); - let ok = program::transfer!(&to, amount); - let after = program::balance!(&to); + let _before = clibc::balance!(&to); + let ok = clibc::transfer!(&to, amount); + let after = clibc::balance!(&to); // Encode success flag in data for easier assertions let mut result = Result::new(ok, if ok { 0 } else { 1 }); diff --git a/crates/examples/src/simple.rs b/crates/examples/src/simple.rs index 76e4e20..71685cc 100644 --- a/crates/examples/src/simple.rs +++ b/crates/examples/src/simple.rs @@ -1,43 +1,44 @@ #![no_std] #![no_main] -extern crate program; -use program::{entrypoint, types::result::Result, require, DataParser}; -use program::types::address::Address; +extern crate clibc; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result}; /// Simple smart contract that compares two 32-bit integers. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates a basic smart contract that: /// - Accepts input data (two 32-bit integers) /// - Performs a simple comparison operation /// - Returns a result with the larger number stored in data -/// +/// /// CONTRACT BEHAVIOR: /// - Takes 8 bytes of input data (two 32-bit integers) /// - Compares the first integer with the second /// - Returns success=true if first > second, success=false otherwise /// - Stores the larger value in the data field -/// +/// /// INPUT FORMAT: The contract expects exactly 8 bytes: /// - Bytes 0-3: First 32-bit integer (little-endian) /// - Bytes 4-7: Second 32-bit integer (little-endian) -/// +/// /// OUTPUT FORMAT: Returns a Result struct with: /// - success: true if first > second, false otherwise /// - error_code: 0 (no error) /// - data_len: 4 (size of u32) /// - data: The larger of the two input values stored as 4 bytes -/// +/// /// REAL-WORLD USAGE: This type of contract could be used for: /// - Simple validation logic /// - Conditional execution based on input values /// - Basic decision-making in decentralized applications -/// +/// /// SECURITY CONSIDERATIONS: /// - Input validation prevents buffer overflows /// - No external calls or state modifications /// - Deterministic execution for all inputs -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // EDUCATIONAL: Validate input data length to prevent buffer overflows // This is a critical security practice in smart contracts require(data.len() >= 8, b"Input data must be at least 8 bytes long"); @@ -50,7 +51,7 @@ fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result // EDUCATIONAL: Perform the comparison and return appropriate result // This demonstrates conditional logic in smart contracts - if first > second { + if first > second { // Return success with the larger number (first) stored in data return Result::with_u32(first); } else { diff --git a/crates/examples/src/storage.rs b/crates/examples/src/storage.rs index 4b3187f..53b62c2 100644 --- a/crates/examples/src/storage.rs +++ b/crates/examples/src/storage.rs @@ -1,10 +1,10 @@ #![no_std] #![no_main] -extern crate program; -use program::{entrypoint, types::result::Result, require}; -use program::types::address::Address; -use program::persist_struct; +extern crate clibc; +use clibc::persist_struct; +use clibc::types::address::Address; +use clibc::{entrypoint, require, types::result::Result}; // Struct 1: User profile persist_struct!(User { @@ -19,13 +19,20 @@ persist_struct!(Config { timeout_ms: u64, }); -fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, _data: &[u8]) -> Result { // --- User --- - require(User::load().is_none() == true, b"user already exists"); - let mut user = User{id: 1000, active: false, level: 3}; + require( + User::load(&program).is_none() == true, + b"user already exists", + ); + let mut user = User { + id: 1000, + active: false, + level: 3, + }; user.level = 4; user.id = 40000; - user.store(); + user.store(&program); // ... change local copy ... user.level = 5; @@ -33,16 +40,22 @@ fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result // ... later ... - let reloaded_user = User::load().expect("user not found"); + let reloaded_user = User::load(&program).expect("user not found"); require(reloaded_user.level == 4, b"user level must be 4"); require(reloaded_user.id == 40000, b"user id must be 40000"); // --- Config --- - require(Config::load().is_none() == true, b"config already exists"); - let mut config = Config{retries: 10, timeout_ms: 10}; + require( + Config::load(&program).is_none() == true, + b"config already exists", + ); + let mut config = Config { + retries: 10, + timeout_ms: 10, + }; config.retries = 13; config.timeout_ms = 100000; - config.store(); + config.store(&program); // ... change local copy ... config.retries = 15; @@ -50,9 +63,12 @@ fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result // ... later ... - let reloaded_config = Config::load().expect("config not found"); + let reloaded_config = Config::load(&program).expect("config not found"); require(reloaded_config.retries == 13, b"config retries must be 13"); - require(reloaded_config.timeout_ms == 100000, b"config timeout_ms must be 100000"); + require( + reloaded_config.timeout_ms == 100000, + b"config timeout_ms must be 100000", + ); Result::new(true, 0) } diff --git a/crates/examples/tests/binary_comparison_test.rs b/crates/examples/tests/binary_comparison_test.rs deleted file mode 100644 index 66c482c..0000000 --- a/crates/examples/tests/binary_comparison_test.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::fs; -use std::path::Path; -use std::rc::Rc; -use core::cell::RefCell; -use core::fmt::Write; - -// Import the test runner and related modules -#[path = "examples_test.rs"] -mod examples_test; - -#[path = "common/utils.rs"] -mod utils; - -use examples_test::TestRunner; -use k256::ecdsa::{signature::hazmat::PrehashVerifier, VerifyingKey, Signature}; -use examples_test::build_ecdsa_payload; - -#[test] -fn test_vm_binary_comparison() -> Result<(), String> { - println!("\n=== VM Binary Comparison Test ===\n"); - - // Step 1: Create TestRunner with file output - let vm_log_path = "/tmp/vm_binary_comparison.log"; - println!("Step 1: Running TestRunner with file output to: {}", vm_log_path); - - // Create a file writer for the TestRunner - let file = fs::File::create(vm_log_path) - .map_err(|e| format!("Failed to create log file: {}", e))?; - - // Create a Write adapter for the file - struct FileWriter(fs::File); - - impl Write for FileWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - use std::io::Write; - self.0.write_all(s.as_bytes()).map_err(|_| core::fmt::Error)?; - self.0.flush().map_err(|_| core::fmt::Error)?; - Ok(()) - } - } - - let writer: Rc> = Rc::new(RefCell::new(FileWriter(file))); - - // Create TestRunner with file output and verbose mode for instruction tracing - let runner = TestRunner::with_writer(writer) - .with_verbose(true) // Enable verbose mode for PC traces - .with_memory_size(512 * 1024) // Larger memory for crypto-heavy binaries - .with_max_pages(128); - - // Run all test cases - runner.execute()?; - - println!("βœ… TestRunner execution completed"); - - // Step 2: Verify the log file was created - let log_size = fs::metadata(vm_log_path) - .map_err(|e| format!("Failed to read log file metadata: {}", e))? - .len(); - - println!("Step 2: VM log file created, size: {} bytes", log_size); - - // Step 3: Parse the log to extract test cases and instructions - let log_content = fs::read_to_string(vm_log_path) - .map_err(|e| format!("Failed to read log file: {}", e))?; - - let test_cases = extract_test_cases(&log_content); - println!("\nStep 3: Extracted {} test cases from log", test_cases.len()); - - // Step 4: Check for corresponding ELF binaries - let binaries_dir = Path::new("../../target/avm32/release"); - - println!("\nStep 4: Checking for ELF binaries in: {}", binaries_dir.display()); - - let mut comparison_results = Vec::new(); - - for test_case in &test_cases { - println!("\n Processing test case: {}", test_case.name); - - // Extract binary mappings - for (address, binary_name) in &test_case.address_mappings { - println!(" Address {} -> Binary: {}", address, binary_name); - - let elf_path = binaries_dir.join(binary_name); - - if elf_path.exists() { - println!(" βœ… ELF found: {}", elf_path.display()); - - // Here we would run the actual comparison - // For this test, we'll just verify the structure - let result = ComparisonResult { - test_name: test_case.name.clone(), - binary_name: binary_name.clone(), - vm_instructions: test_case.instructions.len(), - elf_found: true, - match_percentage: calculate_match_percentage(&test_case.instructions), - }; - - comparison_results.push(result); - } else { - println!(" ⚠️ ELF not found: {}", elf_path.display()); - - let result = ComparisonResult { - test_name: test_case.name.clone(), - binary_name: binary_name.clone(), - vm_instructions: test_case.instructions.len(), - elf_found: false, - match_percentage: 0.0, - }; - - comparison_results.push(result); - } - } - } - - // Step 5: Generate summary report - println!("\n{}", "=".repeat(50)); - println!("COMPARISON SUMMARY"); - println!("{}", "=".repeat(50)); - - let mut all_100_percent = true; - let mut total_instructions = 0; - - for result in &comparison_results { - println!("\nπŸ“Š {} ({})", result.test_name, result.binary_name); - println!(" VM Instructions: {}", result.vm_instructions); - println!(" ELF Found: {}", if result.elf_found { "Yes" } else { "No" }); - - if result.elf_found { - println!(" Match: {:.1}%", result.match_percentage); - - if result.match_percentage < 100.0 { - all_100_percent = false; - } - } else { - all_100_percent = false; - } - - total_instructions += result.vm_instructions; - } - - println!("\n{}", "=".repeat(50)); - println!("Total VM instructions traced: {}", total_instructions); - - // Check if all required binaries were found and matched - let binaries_found = comparison_results.iter().filter(|r| r.elf_found).count(); - let total_test_cases = comparison_results.len(); - - // Define cleanup function - let cleanup = || { - if let Err(e) = fs::remove_file(vm_log_path) { - // Silently ignore if file doesn't exist or can't be removed - // Only print if there's an unexpected error - if e.kind() != std::io::ErrorKind::NotFound { - println!("Note: Could not remove temporary log file: {}", e); - } - } - }; - - if comparison_results.is_empty() { - cleanup(); - return Err("No test cases found in VM log".to_string()); - } - - if binaries_found == 0 { - println!("⚠️ Warning: No ELF binaries found for any of the {} test cases", total_test_cases); - println!(" To build binaries, run: cargo build -p examples --release --target crates/compiler/targets/avm32.json --features binaries"); - println!(" Skipping binary comparison validation."); - println!("\nβœ… Test completed (skipped binary validation)"); - cleanup(); - return Ok(()); - } - - if !all_100_percent { - cleanup(); - return Err(format!( - "Not all binaries achieved 100% match. Found {}/{} binaries, not all matched perfectly", - binaries_found, total_test_cases - )); - } - - println!("πŸŽ‰ All {} found binaries matched 100% with VM execution!", binaries_found); - println!("\nβœ… Binary comparison test completed successfully!"); - - cleanup(); - Ok(()) -} - -#[derive(Debug)] -struct TestCase { - name: String, - address_mappings: Vec<(String, String)>, - instructions: Vec, -} - -#[derive(Debug)] -#[allow(dead_code)] -struct Instruction { - pc: u32, - bytes: Vec, - mnemonic: String, -} - -#[derive(Debug)] -struct ComparisonResult { - test_name: String, - binary_name: String, - vm_instructions: usize, - elf_found: bool, - match_percentage: f64, -} - -fn extract_test_cases(log_content: &str) -> Vec { - let mut test_cases = Vec::new(); - let mut current_test: Option = None; - let mut in_test = false; - - for line in log_content.lines() { - // Detect test case start - if line.contains("#### Running test case:") { - // Save previous test if exists - if let Some(test) = current_test.take() { - test_cases.push(test); - } - - // Extract test name - let name = line - .split("#### Running test case:") - .nth(1) - .unwrap_or("") - .trim() - .trim_end_matches("####") - .trim() - .to_string(); - - current_test = Some(TestCase { - name, - address_mappings: Vec::new(), - instructions: Vec::new(), - }); - in_test = true; - } - - // Extract address mappings - if in_test && line.contains("->") && !line.contains("Binary Mappings:") { - if let Some(test) = current_test.as_mut() { - let parts: Vec<&str> = line.split("->").collect(); - if parts.len() == 2 { - let address = parts[0].trim().to_string(); - let binary = parts[1].trim().to_string(); - test.address_mappings.push((address, binary)); - } - } - } - - // Extract instructions - if line.starts_with("PC = ") { - if let Some(test) = current_test.as_mut() { - if let Some(instr) = parse_instruction_line(line) { - test.instructions.push(instr); - } - } - } - - // Detect test case end - if line.contains("Execution terminated") { - in_test = false; - } - } - - // Save last test if exists - if let Some(test) = current_test { - test_cases.push(test); - } - - test_cases -} - -fn parse_instruction_line(line: &str) -> Option { - // Parse lines like: PC = 0x00000400, Bytes = [13 01 01 fe], Instr = addi x2, x2, -32 - - let pc_part = line.split(", Bytes").next()?; - let pc_str = pc_part.strip_prefix("PC = 0x")?; - let pc = u32::from_str_radix(pc_str, 16).ok()?; - - let bytes_part = line.split("Bytes = [").nth(1)?; - let bytes_str = bytes_part.split(']').next()?; - let bytes: Vec = bytes_str - .split_whitespace() - .filter_map(|b| u8::from_str_radix(b, 16).ok()) - .collect(); - - let instr_part = line.split("Instr = ").nth(1)?; - let mnemonic = instr_part.to_string(); - - Some(Instruction { pc, bytes, mnemonic }) -} - -fn calculate_match_percentage(instructions: &[Instruction]) -> f64 { - // In a real implementation, this would compare with actual ELF instructions - // For this test, we'll simulate that only binaries with sufficient instructions match - if instructions.is_empty() { - 0.0 - } else if instructions.len() < 100 { - // Small instruction count suggests incomplete execution - 50.0 - } else { - // Assume good match for substantial executions - 100.0 - } -} diff --git a/crates/examples/tests/common/ecdsa.rs b/crates/examples/tests/common/ecdsa.rs deleted file mode 100644 index aefb505..0000000 --- a/crates/examples/tests/common/ecdsa.rs +++ /dev/null @@ -1,50 +0,0 @@ -use k256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey}; - -// Fixed test private key (random-looking, non-trivial scalar) -pub const ECDSA_SK_BYTES: [u8; 32] = [ - 0x79, 0x6d, 0x89, 0x3e, 0x8f, 0x16, 0x29, 0x5a, - 0xda, 0xfe, 0x04, 0x8c, 0x53, 0x2f, 0xf9, 0x7e, - 0x47, 0x22, 0x92, 0x1a, 0x86, 0xd2, 0xb4, 0x52, - 0x38, 0xa1, 0x6c, 0x9e, 0x1b, 0x45, 0xd3, 0x7c, -]; -pub const ECDSA_HASH: [u8; 32] = [ - 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, - 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, - 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, - 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, -]; - -// Compressed SEC1 encoding of the corresponding public key -pub const ECDSA_PK_BYTES: [u8; 33] = [ - 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, - 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, - 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, - 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, - 0x0c, -]; - -// Deterministic signature over ECDSA_HASH with ECDSA_SK_BYTES (r || s), big-endian. -pub const ECDSA_SIG_BYTES: [u8; 64] = [ - 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, - 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, - 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, - 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, - 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, - 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, - 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, - 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, -]; - -pub fn build_ecdsa_payload() -> Vec { - // Deterministic key/signature for testing - let pk = ECDSA_PK_BYTES; - let sig = ECDSA_SIG_BYTES; - let hash = ECDSA_HASH; - - let mut payload = Vec::with_capacity(1 + pk.len() + sig.len() + hash.len()); - payload.push(pk.len() as u8); - payload.extend_from_slice(&pk); - payload.extend_from_slice(&sig); - payload.extend_from_slice(&hash); - payload -} diff --git a/crates/examples/tests/common/state.rs b/crates/examples/tests/common/state.rs deleted file mode 100644 index 6728a8a..0000000 --- a/crates/examples/tests/common/state.rs +++ /dev/null @@ -1,19 +0,0 @@ -use state::State; -use super::utils::to_address; - -/// Build a test state with prefunded accounts. -pub fn test_state() -> State { - let mut state = State::new(); - - // Prefund the addresses used in examples tests. - for addr_hex in [ - "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2", - "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3", - ] { - let addr = to_address(addr_hex); - let account = state.get_account_mut(&addr); - account.balance = 1_000_000_000u128; // 1 billion am for testing - } - - state -} diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs deleted file mode 100644 index 2a3073c..0000000 --- a/crates/examples/tests/common/test_runner.rs +++ /dev/null @@ -1,210 +0,0 @@ -#![allow(dead_code)] - -use avm::avm::AVM; -use state::State; -use super::utils::to_address; -use super::state_helper::test_state; -use std::rc::Rc; -use core::cell::RefCell; -use core::fmt::Write; -use std::fs::File; -use std::io::Write as IoWrite; -use std::path::Path; - -// File writer for logging to disk -struct FileWriter { - file: File, -} - -impl FileWriter { - fn new(path: &str) -> std::io::Result { - Ok(FileWriter { - file: File::create(path)?, - }) - } -} - -impl Write for FileWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - self.file.write_all(s.as_bytes()).map_err(|_| core::fmt::Error)?; - self.file.flush().map_err(|_| core::fmt::Error)?; - Ok(()) - } -} - -/// Console writer that wraps println! -struct ConsoleWriter; - -impl Write for ConsoleWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - print!("{}", s); - Ok(()) - } -} - -/// Test runner that encapsulates test execution with configurable output -pub struct TestRunner { - writer: Rc>, - verbose: bool, - vm_memory_size: usize, - max_memory_pages: usize, -} - -impl TestRunner { - /// Create a new test runner with console output (default) - pub fn new() -> Self { - Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) - } - - /// Set VM memory size - pub fn with_memory_size(mut self, size: usize) -> Self { - self.vm_memory_size = size; - self - } - - /// Set max memory pages - pub fn with_max_pages(mut self, pages: usize) -> Self { - self.max_memory_pages = pages; - self - } - - /// Enable or disable verbose mode - pub fn with_verbose(mut self, verbose: bool) -> Self { - self.verbose = verbose; - self - } - - /// Create a test runner with file output - pub fn with_file>(path: P) -> std::io::Result { - let file_writer = FileWriter::new(path.as_ref().to_str().unwrap())?; - Ok(Self::with_writer(Rc::new(RefCell::new(file_writer)))) - } - - /// Create a test runner with a custom writer - pub fn with_writer(writer: Rc>) -> Self { - TestRunner { - writer, - verbose: false, - vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC - max_memory_pages: 128, // allow more pages for larger programs - } - } - - /// Execute all test cases - pub fn execute(&self) -> Result<(), String> { - use super::TEST_CASES; - - writeln!(self.writer.borrow_mut(), "=== Starting Test Run ===").unwrap(); - writeln!(self.writer.borrow_mut(), "Verbose logging: {}", if self.verbose { "enabled" } else { "disabled" }).unwrap(); - - for case in TEST_CASES.iter() { - self.run_test_case(case)?; - } - - // Write test summary - writeln!(self.writer.borrow_mut(), "\n=== Test Run Complete ===").unwrap(); - writeln!(self.writer.borrow_mut(), "Total test cases: {}", TEST_CASES.len()).unwrap(); - - Ok(()) - } - - /// Run a single test case - fn run_test_case(&self, case: &super::TestCase) -> Result<(), String> { - let transactions = case.bundle.transactions.clone(); - let test_state = super::state_helper::test_state(); - let mut avm = AVM::new(self.max_memory_pages, self.vm_memory_size, test_state); - - // Set up AVM with the chosen writer and verbosity - avm.set_verbosity(self.verbose); - avm.set_verbose_writer(self.writer.clone()); - - // Write test case header - writeln!(self.writer.borrow_mut(), "\n############################################").unwrap(); - writeln!(self.writer.borrow_mut(), "#### Running test case: {} ####", case.name).unwrap(); - writeln!(self.writer.borrow_mut(), "############################################").unwrap(); - - // Print address to binary mappings - if !case.address_mappings.is_empty() { - writeln!(self.writer.borrow_mut(), "\nπŸ“ Address -> Binary Mappings:").unwrap(); - for (addr, binary) in &case.address_mappings { - writeln!(self.writer.borrow_mut(), " {} -> {}", addr, binary).unwrap(); - } - } - writeln!(self.writer.borrow_mut()).unwrap(); - - let mut last_success: bool = false; - let mut last_error_code: u32 = 0; - let mut last_result: Option = None; - - for tx in transactions { - // Log the transaction details - writeln!(self.writer.borrow_mut(), - "Running {:?} tx:\n From: {:?}\n To: {:?}\n Data len: {:?}", - tx.tx_type, tx.from, tx.to, tx.data.len() - ).unwrap(); - - let receipt = avm.run_tx(tx); - last_success = receipt.result.success; - last_error_code = receipt.result.error_code; - last_result = Some(receipt.result.clone()); - - // Write state dump - writeln!(self.writer.borrow_mut(), "--- State Dump ---").unwrap(); - for (address, account) in &avm.state.accounts { - writeln!(self.writer.borrow_mut(), " πŸ”‘ Address: 0x{}", address).unwrap(); - writeln!(self.writer.borrow_mut(), " - Balance: {}", account.balance).unwrap(); - writeln!(self.writer.borrow_mut(), " - Nonce: {}", account.nonce).unwrap(); - writeln!(self.writer.borrow_mut(), " - Is contract?: {}", account.is_contract).unwrap(); - writeln!(self.writer.borrow_mut(), " - Code size: {} bytes", account.code.len()).unwrap(); - writeln!(self.writer.borrow_mut(), " - Storage:").unwrap(); - for (key, value) in &account.storage { - writeln!(self.writer.borrow_mut(), " [{:?}] = {:?}", key, value).unwrap(); - } - writeln!(self.writer.borrow_mut(), "").unwrap(); - } - writeln!(self.writer.borrow_mut(), "--------------------").unwrap(); - - // Write receipt - if let Some(abi) = &case.abi { - let mut writer = self.writer.borrow_mut(); - writeln!(writer, "=== Transaction Receipt ===").unwrap(); - writeln!(writer, "From: {:?}", receipt.tx.from).unwrap(); - writeln!(writer, "To: {:?}", receipt.tx.to).unwrap(); - writeln!(writer, "Result: {:?}", receipt.result).unwrap(); - writeln!(writer, "Events:").unwrap(); - receipt.print_events_pretty(abi, &mut *writer); - } else { - writeln!(self.writer.borrow_mut(), "{}", receipt).unwrap(); - } - } - - // Perform assertions - if last_success != case.expected_success { - return Err(format!("{}: expected success={}, got={}", - case.name, case.expected_success, last_success)); - } - - if last_error_code != case.expected_error_code { - return Err(format!("{}: expected error_code={}, got={}", - case.name, case.expected_error_code, last_error_code)); - } - - // Check expected data if specified - if let Some(expected_data) = &case.expected_data { - if let Some(result) = last_result { - let actual_data = &result.data[..result.data_len as usize]; - if actual_data != expected_data.as_slice() { - return Err(format!("{}: expected data mismatch", case.name)); - } - } - } - - Ok(()) - } -} - -impl Default for TestRunner { - fn default() -> Self { - Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) - } -} diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs deleted file mode 100644 index ebede90..0000000 --- a/crates/examples/tests/common/utils.rs +++ /dev/null @@ -1,142 +0,0 @@ -use types::address::Address; -use std::fs; -use std::path::Path; -use compiler::elf::parse_elf_from_bytes; -use avm::global::Config; -use compiler::{EventParam, EventAbi, ParamType}; -use serde_json::Value; - -pub fn to_address(hex: &str) -> Address { - assert!(hex.len() == 40, "Hex string must be exactly 40 characters"); - - fn from_hex_char(c: u8) -> u8 { - match c { - b'0'..=b'9' => c - b'0', - b'a'..=b'f' => c - b'a' + 10, - b'A'..=b'F' => c - b'A' + 10, - _ => panic!("Invalid hex character"), - } - } - - let mut bytes = [0u8; 20]; - let hex_bytes = hex.as_bytes(); - for i in 0..20 { - let hi = from_hex_char(hex_bytes[i * 2]); - let lo = from_hex_char(hex_bytes[i * 2 + 1]); - bytes[i] = (hi << 4) | lo; - } - - Address(bytes) -} - -pub fn load_abi_from_file>(path: P) -> Option> { - let content = fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("❌ Failed to read ABI file from {}", path.as_ref().display())); - - let json: Value = serde_json::from_str(&content) - .unwrap_or_else(|_| panic!("❌ Failed to parse ABI JSON from {}", path.as_ref().display())); - - let events = json.get("events")?; - let events_array = events.as_array()?; - - let mut event_abis = Vec::new(); - for event in events_array { - let name = event.get("name")?.as_str()?.to_string(); - let inputs = event.get("inputs")?.as_array()?; - - let mut params = Vec::new(); - for input in inputs { - let param_name = input.get("name")?.as_str()?.to_string(); - let param_type_str = input.get("type")?.as_str()?; - let indexed = input.get("indexed").and_then(|v| v.as_bool()).unwrap_or(false); - - let param_type = match param_type_str { - "address" => ParamType::Address, - "uint32" => ParamType::Uint(32), - "uint64" => ParamType::Uint(64), - "uint128" => ParamType::Uint(128), - "uint256" => ParamType::Uint(256), - "bool" => ParamType::Bool, - "string" => ParamType::String, - "bytes" => ParamType::Bytes, - _ => panic!("❌ Unsupported parameter type: {}", param_type_str), - }; - - params.push(EventParam { - name: param_name, - kind: param_type, - indexed, - }); - } - - event_abis.push(EventAbi { - name, - inputs: params, - }); - } - - Some(event_abis) -} - -/// Load and merge multiple ABI files (events only). Returns None if no events found. -pub fn load_abis_from_files(paths: &[&str]) -> Option> { - let mut merged = Vec::new(); - for path in paths { - if let Some(mut events) = load_abi_from_file(path) { - merged.append(&mut events); - } - } - if merged.is_empty() { - None - } else { - Some(merged) - } -} - -pub fn get_program_code(name: &str) -> Vec { - // Build the full path - let bin_path = format!("bin/{}", name); - - // Try reading from bin directory first (for compiled binaries) - let bytes = fs::read(&bin_path) - .or_else(|_| { - // Fallback to target directory for development - let target_path = format!("../../target/avm32/release/{}", name); - fs::read(&target_path) - }) - .unwrap_or_else(|_| panic!("❌ Failed to read ELF file: {}", name)); - - let elf = parse_elf_from_bytes(&bytes) - .unwrap_or_else(|_| panic!("❌ Failed to parse ELF from {}", name)); - - let (code, code_start) = elf - .get_flat_code() - .unwrap_or_else(|| panic!("❌ No code sections found in ELF {}", name)); - - let (rodata, rodata_start) = elf - .get_flat_rodata() - .unwrap_or_else(|| { - (vec![], usize::MAX as u64) - }); - - // assert sizes - assert!(code.len() <= Config::CODE_SIZE_LIMIT, "code size exceeds limit"); - assert!(rodata.len() <= Config::RO_DATA_SIZE_LIMIT, "read only data size exceeds limit"); - - let mut total_len = code_start + code.len() as u64; // assumes rodata is after code - if rodata.len() > 0 { - total_len = rodata_start + rodata.len() as u64; // assumes rodata is after code - } - - // Initialize memory with 0x00 - let mut combined = vec![0u8; total_len as usize]; - - // Copy code - combined[code_start as usize..code_start as usize + code.len()].copy_from_slice(&code); - - // Copy rodata (if it exists) - if rodata.len() > 0 { - combined[rodata_start as usize..rodata_start as usize + rodata.len()].copy_from_slice(&rodata); - } - combined -} diff --git a/crates/examples/tests/ecdsa_payload_test.rs b/crates/examples/tests/ecdsa_payload_test.rs deleted file mode 100644 index d9365ce..0000000 --- a/crates/examples/tests/ecdsa_payload_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, SigningKey, VerifyingKey}; - -#[path = "common/ecdsa.rs"] -mod ecdsa; - -#[test] -fn test_ecdsa_payload_is_valid() { - // Build payload using the shared helper. - let payload = ecdsa::build_ecdsa_payload(); - - let pk_len = payload[0] as usize; - let pubkey = &payload[1..1 + pk_len]; - let sig = &payload[1 + pk_len..1 + pk_len + 64]; - let hash = &payload[1 + pk_len + 64..]; - - // Ensure the public key matches the hardcoded secret key we use for signing. - let signing_key = - SigningKey::from_bytes(&ecdsa::ECDSA_SK_BYTES.into()).expect("valid sk bytes"); - let expected_vk = signing_key.verifying_key(); - let payload_vk = VerifyingKey::from_sec1_bytes(pubkey).expect("valid pubkey in payload"); - assert_eq!( - expected_vk.to_encoded_point(true), - payload_vk.to_encoded_point(true), - "payload pubkey should match the signing key" - ); - - // Verify the signature against the known hash. - let sig = Signature::from_slice(sig).expect("valid signature bytes"); - payload_vk - .verify_prehash(hash, &sig) - .expect("signature should verify for payload hash"); - - // Double-check we used the expected hash constant. - assert_eq!( - hash, - &ecdsa::ECDSA_HASH, - "payload hash should match the test constant" - ); -} diff --git a/crates/examples/tests/examples_test.rs b/crates/examples/tests/examples_test.rs deleted file mode 100644 index a702360..0000000 --- a/crates/examples/tests/examples_test.rs +++ /dev/null @@ -1,572 +0,0 @@ -#[path = "common/utils.rs"] -mod utils; - -#[path = "common/test_runner.rs"] -mod test_runner; - -#[path = "common/state.rs"] -mod state_helper; - -#[path = "common/ecdsa.rs"] -mod ecdsa; - -use avm::transaction::{TransactionType, TransactionBundle, Transaction}; -use avm::router::{encode_router_calls, HostFuncCall}; -use once_cell::sync::Lazy; -use compiler::EventAbi; -pub use ecdsa::{build_ecdsa_payload, ECDSA_HASH, ECDSA_SK_BYTES}; -pub use test_runner::TestRunner; -use utils::{to_address, load_abi_from_file, load_abis_from_files, get_program_code}; - -/// Centralized ELF binary paths for testing -pub struct ElfBinary { - pub name: &'static str, - pub path: &'static str, - pub description: &'static str, -} - -/// All ELF binaries used in tests -pub const ELF_BINARIES: &[ElfBinary] = &[ - ElfBinary { - name: "simple", - path: "bin/simple", - description: "Simple test program", - }, - ElfBinary { - name: "multi_func", - path: "bin/multi_func", - description: "Multiple function test", - }, - ElfBinary { - name: "logging", - path: "bin/logging", - description: "Logging functionality test", - }, - ElfBinary { - name: "storage", - path: "bin/storage", - description: "Storage operations test", - }, - ElfBinary { - name: "call_program", - path: "bin/call_program", - description: "Program calling test", - }, - ElfBinary { - name: "erc20", - path: "bin/erc20", - description: "ERC20 token contract", - }, - ElfBinary { - name: "lib_import", - path: "bin/lib_import", - description: "Library import test", - }, - ElfBinary { - name: "allocator_demo", - path: "bin/allocator_demo", - description: "Memory allocator demonstration", - }, - ElfBinary { - name: "native_transfer", - path: "bin/native_transfer", - description: "Native token transfer via syscall", - }, - ElfBinary { - name: "dex", - path: "bin/dex", - description: "Simple AMM between AM and ERC20", - }, -]; - -/// Get an ELF binary by name -pub fn get_elf_by_name(name: &str) -> Option<&'static ElfBinary> { - ELF_BINARIES.iter().find(|elf| elf.name == name) -} - -/// Get the full path for an ELF binary -pub fn get_elf_path(name: &str) -> Option { - get_elf_by_name(name).map(|elf| format!("crates/examples/{}", elf.path)) -} - - -#[derive(Debug)] -pub struct TestCase<'a> { - pub name: &'a str, - pub expected_success: bool, - pub expected_error_code: u32, - pub expected_data: Option>, - pub bundle: TransactionBundle, - pub abi: Option>, - pub address_mappings: Vec<(&'a str, &'a str)>, // (address, binary_name) -} - -pub static TEST_CASES: Lazy>> = Lazy::new(|| { - vec![ - TestCase { - name: "erc20", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![128, 240, 250, 2]), // Expected data: 50,000,000 in little-endian - abi: load_abi_from_file("bin/erc20.abi.json"), - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - data: get_program_code("erc20"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, // initialize - args: (|| { - // max supply - let max_supply: u32 = 100000000; // 100 million - let mut max_supply_bytes: Vec = max_supply.to_le_bytes().to_vec(); - - // decimals - let decimals: u8 = 18; - - // combine - max_supply_bytes.extend(vec![decimals]); - max_supply_bytes - })(), - } - ]), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x02, // transfer - args: (|| { - // to address (20 bytes) - let to_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); - let mut args = to_addr.0.to_vec(); - - // amount (4 bytes) - let amount: u32 = 50000000; // 50 million tokens - args.extend(amount.to_le_bytes()); - - args - })(), - } - ]), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x05, // balance_of - args: (|| { - // check balance of the original caller (d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0) - let owner_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); - owner_addr.0.to_vec() - })(), - } - ]), - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "call program", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "call_program"), - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "simple"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("call_program"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - data: get_program_code("simple"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1").0.to_vec(); - data.extend(vec![100, 0, 0, 0, 42, 0, 0, 0]); - data - })(), - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "account create (storage)", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "storage"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("storage"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: vec![], - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "account create (simple)", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "simple"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("simple"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: vec![ - 100, 0, 0, 0, // first u64 = 100 - 42, 0, 0, 0, // second u64 = 42 - ], - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "multi function (simple)", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "multi_func"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("multi_func"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, - args: vec![ - 100, 0, 0, 0, // first = 100 - 42, 0, 0, 0, // second = 42 - ], - } - ]), - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "allocator demo", - expected_success: true, - expected_error_code: 0, - expected_data: None,//Some(b"VM allocator demo completed successfully!".to_vec()), - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "allocator_demo"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("allocator_demo"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - // 6 x u32 little-endian: - // Vec: 12, 15, 100; Map: 95, 87, 92 - data: vec![ - 12, 0, 0, 0, - 15, 0, 0, 0, - 100, 0, 0, 0, - 95, 0, 0, 0, - 87, 0, 0, 0, - 92, 0, 0, 0, - ], - value: 0, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "native transfer", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::Transfer, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: vec![], - value: 10, - nonce: 0, - }, - ]), - }, - - TestCase { - name: "guest transfer syscall", - expected_success: true, - expected_error_code: 0, - expected_data: Some({ - let mut v = 42u128.to_le_bytes().to_vec(); - v - }), - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4", "native_transfer"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("native_transfer"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0").0.to_vec(); - data.extend_from_slice(&42u64.to_le_bytes()); - data - })(), - value: 0, - nonce: 1, - }, - ]), - }, - - TestCase { - name: "dex amm", - expected_success: true, - expected_error_code: 0, - expected_data: Some({ - let mut buf = Vec::new(); - buf.extend_from_slice(&101000u128.to_le_bytes()); - buf.extend_from_slice(&495050u128.to_le_bytes()); - buf - }), - abi: load_abis_from_files(&["bin/erc20.abi.json", "bin/dex.abi.json"]), - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20"), - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5", "dex"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("erc20"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, // init - args: (|| { - let mut args = Vec::new(); - let supply: u32 = 1_000_000; - args.extend_from_slice(&supply.to_le_bytes()); - args.push(0); // decimals - args - })(), - } - ]), - value: 0, - nonce: 1, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x02, // transfer - args: (|| { - let mut args = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5").0.to_vec(); - let amount: u32 = 500_000; - args.extend_from_slice(&amount.to_le_bytes()); - args - })(), - } - ]), - value: 0, - nonce: 2, - }, - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("dex"), - value: 0, - nonce: 3, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: { - let mut data = Vec::new(); - data.push(0x01); // add liquidity - data.extend_from_slice(&100_000u64.to_le_bytes()); // AM in - data.extend_from_slice(&500_000u64.to_le_bytes()); // token target - data - }, - value: 0, - nonce: 4, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"), - data: { - let mut data = Vec::new(); - data.push(0x03); // swap - data.push(0x00); // am -> token - data.extend_from_slice(&1_000u64.to_le_bytes()); - data - }, - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: { - let mut data = Vec::new(); - data.push(0x02); // remove liquidity - data.extend_from_slice(&100_000u64.to_le_bytes()); - data - }, - value: 0, - nonce: 5, - }, - ]), - }, - - TestCase { - name: "ecdsa verify", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "ecdsa_verify"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("ecdsa_verify"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: build_ecdsa_payload(), - value: 0, - nonce: 1, - }, - ]), - }, - - ] -}); - -#[test] -fn test_examples() { - TestRunner::default().execute().unwrap() -} diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml new file mode 100644 index 0000000..d280ff1 --- /dev/null +++ b/crates/kernel/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "kernel" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" + +[features] +default = [] +guest_kernel = [] + +[dependencies] +clibc = { path = "../clibc", features = ["kernel"] } +types = { path = "../types" } +state = { path = "../state" } + +[[bin]] +name = "kernel" +path = "src/main.rs" +required-features = ["guest_kernel"] + +##################### +# Kernel Tests +##################### + +[[bin]] +name = "kernel_first_test" +path = "src/memory/tests/first_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_alloc_test" +path = "src/memory/tests/mem_alloc_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_map_test" +path = "src/memory/tests/mem_map_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_map_edge_test" +path = "src/memory/tests/mem_map_edge_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_page_alloc_test" +path = "src/memory/tests/page_alloc_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_heap_edge_test" +path = "src/memory/tests/heap_edge_test.rs" +required-features = ["guest_kernel"] diff --git a/crates/kernel/README.md b/crates/kernel/README.md new file mode 100644 index 0000000..c698d2c --- /dev/null +++ b/crates/kernel/README.md @@ -0,0 +1,175 @@ +# Kernel crate + +This crate implements the guest kernel that runs inside the VM. It is +responsible for bootstrapping the system, managing address spaces, launching +user programs, handling traps and syscalls, and returning receipts/results to +the bootloader. + +## High-level responsibilities + +- Boot-time initialization from `BootInfo` (page tables, heap, state). +- Page allocation and page-table mapping. +- Task creation and scheduling (single-threaded, cooperative). +- Trap entry/exit and syscall dispatch. +- User program loading, execution, and result collection. +- Kernel storage and receipt management. + +## Boot and initialization flow + +1) Bootloader writes `BootInfo` into guest memory and jumps to kernel entry. +2) `init_kernel` reads `BootInfo`, sets `ROOT_PPN`, initializes allocator + state, and sets up the kernel heap window. +3) Trap vector is installed and the kernel starts processing transaction + bundles. Each bundle is decoded and executed by creating tasks. + +Key files: +- `src/init.rs`: kernel init, boot info parsing. +- `src/memory/`: allocator and page table helpers. +- `src/trap/`: trap entry and syscall dispatch. +- `src/task/`: task creation, scheduling, and context switch. + +## Task model + +A task represents an isolated user program execution with its own: +- Address space (root page table, ASID). +- Trapframe (PC, SP, argument registers). +- User heap pointer. +- Caller info (task id). + +Core types: +- `Task` (`src/task/task.rs`): task state and context. +- `AddressSpace` (`src/task/task.rs`): root PPN + user VA window info. + +### Task lifecycle + +1) **Creation** + - A fresh ASID and root page table are allocated. + - The user program window is mapped with user permissions. + - A dedicated call-args page is mapped just above the user window. + - Program bytes are copied into the user window. + - Call arguments (to/from/input) are copied into the call-args page. + - The trapframe is initialized (PC, SP, A0..A3). + +2) **Run** + - `run_task` switches `satp` to the task root and jumps to user PC. + - Syscalls trap back into the kernel via the trampoline page. + +3) **Syscall handling** + - The trap handler switches to kernel context, decodes syscall number, + and dispatches to the appropriate handler in `src/syscall/`. + - Results are written into the task context and returned to user. + +4) **Completion** + - The user program exits or triggers a breakpoint return. + - The kernel collects the result payload and writes a receipt. + - The kernel switches back to the caller task or the kernel task. + +Task lifetime diagram: + +``` + +---------+ +-----------+ +-------------+ +------------+ + | created | ---> | runnable | ---> | running | ---> | completed | + +---------+ +-----------+ +-------------+ +------------+ + ^ | + | v + +-------------- syscall/trap ----------+ +``` + +## Memory layout + +The kernel uses a flat VA window (from `BootInfo`) to access its heap and +static memory. User programs run in a separate user VA window starting at 0. + +### User program VA window + +Defined in `src/global.rs`: + +- `PROGRAM_VA_BASE`: base of user mappings (0x0). +- `PROGRAM_WINDOW_BYTES`: total mapped user window size. +- `CODE_SIZE_LIMIT`: max code size. +- `RO_DATA_SIZE_LIMIT`: reserved rodata size. +- `HEAP_BYTES`: user heap size. +- `STACK_BYTES`: user stack size. +- `HEAP_START_ADDR`: heap base within the user window. + +The stack is placed at the end of the user window and grows downward: + +``` +low VA high VA +| code | rodata | heap .............. | stack (grows down) | +^ PROGRAM_VA_BASE ^ stack_base ^ stack_top +``` + +### Call-args page + +Call arguments (to/from addresses and input buffer) live in a dedicated page +mapped just above the user window: + +``` +CALL_ARGS_PAGE_BASE = PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES +TO_PTR_ADDR = CALL_ARGS_PAGE_BASE + 0x100 +FROM_PTR_ADDR = TO_PTR_ADDR + ADDRESS_LEN +INPUT_BASE_ADDR = FROM_PTR_ADDR + ADDRESS_LEN +``` + +This keeps call-args separate from user heap/stack and avoids corruption. +The call-args page is mapped as user-read only. + +User memory map (not to scale): + +``` +VA 0x00000000 + | code | rodata | heap .............. | stack | call-args page | + ^ PROGRAM_VA_BASE ^ stack_top ^ CALL_ARGS_PAGE_BASE +VA 0x00000000 + PROGRAM_WINDOW_BYTES (end of user window) +``` + +### Kernel heap + +The kernel heap is a bump allocator initialized from `BootInfo.heap_ptr` and +bounded by the kernel VA window (`va_base .. va_base + va_len`). If the kernel +heap exhausts, the allocator panics. + +## Trap and syscall flow + +1) User executes an `ecall` or trap instruction. +2) Control transfers to the trampoline page (`TRAP_TRAMPOLINE_VA`). +3) Kernel trap handler saves user state and dispatches syscalls. +4) Syscall handlers live in `src/syscall/` and may read user memory using + the task root page table. +5) Return values are placed in the trapframe and execution resumes in user. + +Trap cycle diagram: + +``` + user code + | + | ecall / fault + v + TRAP_TRAMPOLINE_VA ---> kernel trap handler ---> syscall handler + ^ | + | v + return to user <----------- update trapframe <---+ +``` + +## Storage and receipts + +Kernel storage is maintained in the global `State` object. Syscalls can read +and write key/value pairs. Transaction receipts are written as tasks complete +and returned to the bootloader. + +## Debugging notes + +- Stack/heap overlap bugs are common if `stack_top` is not placed at the end + of the user window. +- Call-args memory should be isolated from user heap/stack. +- Kernel heap exhaustion will panic in `src/memory/heap.rs`. + +## Relevant files + +- `src/global.rs`: global constants and kernel-wide state. +- `src/task/prep.rs`: program loading and trapframe setup. +- `src/task/run.rs`: context switch and run loop. +- `src/trap/mod.rs`: trap entry/exit and syscall dispatch. +- `src/syscall/`: syscall implementations. +- `src/memory/page_allocator.rs`: page allocation and mapping. diff --git a/crates/kernel/src/bundle/create_account.rs b/crates/kernel/src/bundle/create_account.rs new file mode 100644 index 0000000..f7ba086 --- /dev/null +++ b/crates/kernel/src/bundle/create_account.rs @@ -0,0 +1,40 @@ +use kernel::global::{CODE_SIZE_LIMIT, RO_DATA_SIZE_LIMIT, STATE}; +use clibc::logf; +use clibc::parser::HexCodec; +use state::State; +use types::transaction::Transaction; + +pub(crate) fn create_account(tx: &Transaction) { + let code_size = tx.data.len(); + let is_contract = code_size > 0; + + let mut addr_buf = [0u8; 40]; + let addr_hex = HexCodec::encode(tx.to.as_ref(), &mut addr_buf); + logf!( + "Tx creating account at address %s. Is contract: %d. Code size: %d bytes.", + addr_hex.as_ptr() as u32, + addr_hex.len() as u32, + is_contract as u32, + code_size as u32 + ); + + let max = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT; + if code_size > max { + panic!( + "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", + code_size, max + ); + } + + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = state.get_account_mut(&tx.to); + account.code = tx.data.clone(); + account.is_contract = is_contract; + logf!( + "account created in kernel state: addr=%s is_contract=%d code_len=%d", + addr_hex.as_ptr() as u32, + addr_hex.len() as u32, + is_contract as u32, + code_size as u32 + ); +} diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs new file mode 100644 index 0000000..fbb2a45 --- /dev/null +++ b/crates/kernel/src/bundle/mod.rs @@ -0,0 +1,115 @@ +use core::mem::forget; + +extern crate alloc; + +use alloc::vec::Vec; +use clibc::{log, logf}; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; +use types::{Result, TransactionReceipt}; + +use kernel::global::{BUNDLE, CURRENT_TX, RECEIPTS}; + +mod create_account; +mod program_call; +mod transfer; +mod result; + +use self::create_account::create_account; +use self::program_call::program_call; +use self::transfer::transfer; +use self::result::{update_receipt_from_task, write_kernel_result}; + +pub(crate) fn decode_bundle(encoded_bundle: &[u8]) -> bool { + log!("processing transaction bundle"); + if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { + let count = bundle.transactions.len(); + logf!("decoded tx count=%d", count as u32); + let receipts = bundle + .transactions + .iter() + .cloned() + .map(|tx| TransactionReceipt::new(tx, Result::new(true, 0))) + .collect::>(); + unsafe { + *BUNDLE.get_mut() = Some(bundle); + *CURRENT_TX.get_mut() = 0; + *RECEIPTS.get_mut() = Some(receipts); + } + true + } else { + false + } +} + +pub(crate) fn process_bundle() { + let (idx, count) = unsafe { + let count = BUNDLE + .get_mut() + .as_ref() + .map(|bundle| bundle.transactions.len()) + .unwrap_or(0); + (*CURRENT_TX.get_mut(), count) + }; + if idx >= count { + bundle_complete(); + } + logf!("processing tx %d/%d", (idx + 1) as u32, count as u32); + let tx = unsafe { + BUNDLE + .get_mut() + .as_ref() + .and_then(|bundle| bundle.transactions.get(idx)) + }; + if let Some(tx) = tx { + if execute_transaction(tx) { + resume_bundle(); + } + } else { + logf!("missing tx at index %d", idx as u32); + resume_bundle(); + } +} + +pub(crate) extern "C" fn resume_bundle() -> ! { + update_receipt_from_task(); + unsafe { + let curr = *CURRENT_TX.get_mut(); + *CURRENT_TX.get_mut() = curr.wrapping_add(1); + } + process_bundle(); + loop {} +} + +fn execute_transaction(tx: &Transaction) -> bool { + match tx.tx_type { + TransactionType::CreateAccount => { + create_account(tx); + true + } + TransactionType::ProgramCall => { + program_call(tx, resume_bundle); + false + } + TransactionType::Transfer => { + transfer(tx); + true + } + _ => panic!("unsupported transaction type"), + } +} + +fn bundle_complete() -> ! { + log!("transaction bundle complete"); + write_kernel_result(); + // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. + let bundle = unsafe { BUNDLE.get_mut().take() }; + if let Some(bundle) = bundle { + forget(bundle); + } + let receipts = unsafe { RECEIPTS.get_mut().take() }; + if let Some(receipts) = receipts { + forget(receipts); + } + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs new file mode 100644 index 0000000..b059eb9 --- /dev/null +++ b/crates/kernel/src/bundle/program_call.rs @@ -0,0 +1,52 @@ +use kernel::{kernel_run_task, prep_program_task, PROGRAM_WINDOW_BYTES}; +use kernel::global::TASKS; +use kernel::user_program::with_program_image; +use clibc::{log, logf}; +use clibc::parser::HexCodec; +use types::transaction::Transaction; + +pub(crate) fn program_call(tx: &Transaction, resume: extern "C" fn() -> !) { + let mut from_buf = [0u8; 40]; + let mut to_buf = [0u8; 40]; + let from_hex = HexCodec::encode(tx.from.as_ref(), &mut from_buf); + let to_hex = HexCodec::encode(tx.to.as_ref(), &mut to_buf); + let task = with_program_image(&tx.to, |image| { + logf!( + "Program call: from=%s to=%s input_len=%d code_len=%d", + from_hex.as_ptr() as u32, + from_hex.len() as u32, + to_hex.as_ptr() as u32, + to_hex.len() as u32, + tx.data.len() as u32, + image.code.len() as u32 + ); + prep_program_task(&tx.to, &tx.from, image.code, &tx.data, image.entry_off) + }); + + if let Some(task) = task { + logf!( + "Program task created: root=0x%x asid=%d window_size=%d", + task.addr_space.root_ppn, + task.addr_space.asid as u32, + PROGRAM_WINDOW_BYTES as u32 + ); + unsafe { + let tasks_slot = TASKS.get_mut(); + if tasks_slot.push(task).is_err() { + log!("program task list full; skipping run"); + return; + } + let current = tasks_slot.len().saturating_sub(1); + core::arch::asm!( + "mv ra, {resume}", + "j {run}", + run = sym kernel_run_task, + resume = in(reg) resume as usize, + in("a0") current, + options(noreturn), + ); + } + } else { + panic!("program_call: no memory manager installed; cannot create program task"); + } +} diff --git a/crates/kernel/src/bundle/result.rs b/crates/kernel/src/bundle/result.rs new file mode 100644 index 0000000..bf2c5be --- /dev/null +++ b/crates/kernel/src/bundle/result.rs @@ -0,0 +1,100 @@ +use kernel::global::{ + CURRENT_TX, KERNEL_RESULT_ADDR, LAST_COMPLETED_TASK, RECEIPTS, STATE, TASKS, +}; +use kernel::memory::heap; +use clibc::{log, logf}; +use types::{KernelResult, TransactionReceipt}; + +pub(crate) fn update_receipt_from_task() { + let (tx_idx, task_idx) = unsafe { + let tx_idx = *CURRENT_TX.get_mut(); + let task_idx = (*LAST_COMPLETED_TASK.get_mut()).take(); + (tx_idx, task_idx) + }; + let task_idx = match task_idx { + Some(idx) => idx, + None => { + log!("resume_bundle: no completed task to update receipt"); + return; + } + }; + let result = unsafe { + let tasks = TASKS.get_mut(); + tasks + .get(task_idx) + .and_then(|task| task.last_result) + }; + let result = match result { + Some(res) => res, + None => { + log!("resume_bundle: completed task missing result"); + return; + } + }; + unsafe { + if let Some(receipts) = RECEIPTS.get_mut().as_mut() { + if let Some(receipt) = receipts.get_mut(tx_idx) { + receipt.result = result; + } else { + logf!("resume_bundle: invalid receipt index %d", tx_idx as u32); + } + } else { + log!("resume_bundle: receipts missing"); + } + } +} + +pub(crate) fn write_kernel_result() { + let encoded = unsafe { + RECEIPTS + .get_mut() + .as_ref() + .map(|receipts| TransactionReceipt::encode_list(receipts)) + }; + let encoded = match encoded { + Some(data) => data, + None => { + log!("kernel_result: receipts missing"); + return; + } + }; + let len = encoded.len() as u32; + // The bootloader maps the kernel window at VA 0, so this VA is also a + // physical address in the current setup. + let ptr = encoded.as_ptr() as u32; + core::mem::forget(encoded); + let (state_ptr, state_len) = match unsafe { STATE.get_mut().as_ref() } { + Some(state) => { + let len = state.encoded_len(); + if len == 0 { + (0, 0) + } else { + match heap::alloc(len, 8) { + Some(ptr) => { + let buf = unsafe { core::slice::from_raw_parts_mut(ptr, len) }; + match state.encode_into(buf) { + Some(written) => (ptr as u32, written as u32), + None => (0, 0), + } + } + None => (0, 0), + } + } + } + None => (0, 0), + }; + let header = KernelResult { + receipts_ptr: ptr, + receipts_len: len, + state_ptr, + state_len, + }; + unsafe { + core::ptr::write_volatile(KERNEL_RESULT_ADDR as *mut KernelResult, header); + } + logf!( + "kernel_result: receipts_ptr=0x%x receipts_len=%d", + ptr, + len + ); +} diff --git a/crates/kernel/src/bundle/transfer.rs b/crates/kernel/src/bundle/transfer.rs new file mode 100644 index 0000000..510f5c2 --- /dev/null +++ b/crates/kernel/src/bundle/transfer.rs @@ -0,0 +1,27 @@ +use clibc::log; +use kernel::global::{CURRENT_TX, RECEIPTS, STATE}; +use state::State; +use types::Result; +use types::transaction::Transaction; + +const TRANSFER_ERROR: u32 = 1; + +pub(crate) fn transfer(tx: &Transaction) { + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let ok = state.transfer(&tx.from, &tx.to, tx.value); + if !ok { + log!("transfer failed"); + set_receipt(false, TRANSFER_ERROR); + } +} + +fn set_receipt(success: bool, error_code: u32) { + let tx_idx = unsafe { *CURRENT_TX.get_mut() }; + unsafe { + if let Some(receipts) = RECEIPTS.get_mut().as_mut() { + if let Some(receipt) = receipts.get_mut(tx_idx) { + receipt.result = Result::new(success, error_code); + } + } + } +} diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs new file mode 100644 index 0000000..379d796 --- /dev/null +++ b/crates/kernel/src/global.rs @@ -0,0 +1,208 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::cell::UnsafeCell; +use core::mem::MaybeUninit; +use core::ptr; +use state::State; +use types::TransactionReceipt; +use types::{ADDRESS_LEN, SV32_PAGE_SIZE}; +use types::transaction::TransactionBundle; + +use crate::Task; +use crate::memory::heap::BumpAllocator; +use crate::memory::page_allocator::PageAllocator; + +/// Minimal wrapper to store non-`Sync` types in statics. +/// +/// Safety: Callers must guarantee exclusive access when mutating. +pub struct Global { + inner: UnsafeCell, +} + +impl Global { + pub const fn new(value: T) -> Self { + Self { + inner: UnsafeCell::new(value), + } + } + + /// # Safety + /// Callers must ensure exclusive access or otherwise serialize mutations. + pub unsafe fn get_mut(&self) -> &mut T { + unsafe { &mut *self.inner.get() } + } +} + +unsafe impl Sync for Global {} + +// ============================================ +// Program Call Limits and Memory Layout +// ============================================ +/// Maximum input buffer length accepted by program calls. +pub const MAX_INPUT_LEN: usize = 1024; +/// Upper bound for program text + data bytes in a user image. +pub const CODE_SIZE_LIMIT: usize = 0x30000; +/// Reserved space for read-only data in the user window. +pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; +/// User VA base for program mappings. +pub const PROGRAM_VA_BASE: u32 = 0x0; +/// User stack size (bytes). +pub const STACK_BYTES: usize = 0x4000; // 16 KiB user stack +/// User heap size (bytes). +pub const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap +/// Total mapped window for a program: code/rodata, stack, and heap. +pub const PROGRAM_WINDOW_BYTES: usize = align_up( + CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, + SV32_PAGE_SIZE, +); +/// Start of the user heap within the program window. +pub const HEAP_START_ADDR: usize = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + 0x100; +/// Maximum size of a program result payload. +pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; +/// Default program entry address within the user window. +pub const PROGRAM_START_ADDR: u32 = 0x400; +/// Address where program results are written for user-mode reads. +pub const RESULT_ADDR: u32 = 0x100; +/// Kernel VA for the serialized result header handoff. +pub const KERNEL_RESULT_ADDR: u32 = 0x100; +/// User VA base for call arguments placed just above the program window. +pub(crate) const CALL_ARGS_PAGE_BASE: u32 = PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES as u32; +/// User VA where the "to" address bytes are copied for program calls. +pub(crate) const TO_PTR_ADDR: u32 = CALL_ARGS_PAGE_BASE + 0x100; +/// User VA where the "from" address bytes are copied for program calls. +pub(crate) const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; +/// User VA base for the input buffer in the call-args page. +pub(crate) const INPUT_BASE_ADDR: u32 = FROM_PTR_ADDR + ADDRESS_LEN as u32; + +// ============================================ +// Task Scheduling and Bookkeeping +// ============================================ +/// Max number of task slots the kernel tracks at once. +pub const MAX_TASKS: usize = 16; +/// Reserved slot index for the kernel/supervisor task. +pub const KERNEL_TASK_SLOT: usize = 0; +/// Currently running task slot index (kernel or user). +pub static CURRENT_TASK: Global = Global::new(KERNEL_TASK_SLOT); +/// Index of the bundle transaction currently being executed. +pub static CURRENT_TX: Global = Global::new(0); +/// Task slot that most recently completed and returned to the kernel. +/// Used to attach the correct program result to the current receipt. +pub static LAST_COMPLETED_TASK: Global> = Global::new(None); +/// Active receipts buffer being filled while processing a bundle. +pub static RECEIPTS: Global>> = Global::new(None); +/// Currently decoded bundle, if any. +pub static BUNDLE: Global> = Global::new(None); + +// ============================================ +// Task List Storage +// ============================================ +/// Fixed-size task list backing store for scheduler bookkeeping. +pub struct TaskList { + len: usize, + slots: MaybeUninit<[Task; MAX_TASKS]>, +} + +impl TaskList { + pub const fn new() -> Self { + Self { + len: 0, + slots: MaybeUninit::uninit(), + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn push(&mut self, task: Task) -> Result<&Task, Task> { + if self.len >= MAX_TASKS { + return Err(task); + } + let idx = self.len; + unsafe { + let base = self.slots.as_mut_ptr() as *mut Task; + base.add(idx).write(task); + } + self.len += 1; + Ok(unsafe { &*(self.slots.as_ptr() as *const Task).add(idx) }) + } + + pub fn get(&self, idx: usize) -> Option<&Task> { + if idx < self.len { + Some(unsafe { &*(self.slots.as_ptr() as *const Task).add(idx) }) + } else { + None + } + } + + pub fn get_mut(&mut self, idx: usize) -> Option<&mut Task> { + if idx < self.len { + Some(unsafe { &mut *(self.slots.as_mut_ptr() as *mut Task).add(idx) }) + } else { + None + } + } + + pub fn kernel_task(&self) -> Option<&Task> { + self.get(KERNEL_TASK_SLOT) + } + + pub fn set_at(&mut self, idx: usize, task: Task) -> Result<&Task, Task> { + if idx >= MAX_TASKS { + return Err(task); + } + if idx > self.len { + return Err(task); + } + if idx < self.len { + unsafe { ptr::drop_in_place((self.slots.as_mut_ptr() as *mut Task).add(idx)) }; + } else { + self.len += 1; + } + unsafe { + let base = self.slots.as_mut_ptr() as *mut Task; + base.add(idx).write(task); + Ok(&*base.add(idx)) + } + } + + pub fn last(&self) -> Option<&Task> { + if self.len == 0 { + None + } else { + self.get(self.len - 1) + } + } +} + +impl Drop for TaskList { + fn drop(&mut self) { + for idx in 0..self.len { + unsafe { + ptr::drop_in_place((self.slots.as_mut_ptr() as *mut Task).add(idx)); + } + } + } +} + +// ============================================ +// Global Kernel State +// ============================================ +#[allow(dead_code)] +/// Global task list storage. +pub static TASKS: Global = Global::new(TaskList::new()); +/// Global chain state snapshot, if loaded. +pub static STATE: Global> = Global::new(None); +/// Next ASID to assign when launching a program. +pub static NEXT_ASID: Global = Global::new(1); +/// Root physical page number for the kernel address space. +pub static ROOT_PPN: Global = Global::new(0); +/// Page allocator backing store. +pub static PAGE_ALLOC: Global> = Global::new(None); +/// Kernel heap allocator instance. +pub static KERNEL_HEAP: Global = Global::new(BumpAllocator::empty()); + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs new file mode 100644 index 0000000..0e6b31c --- /dev/null +++ b/crates/kernel/src/init.rs @@ -0,0 +1,77 @@ +use core::{cmp, slice}; + +use clibc::{log, logf}; +use state::State; + +use kernel::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; +use kernel::{BootInfo, Task, trap}; +use kernel::memory::{heap, page_allocator}; + +/// Initialize kernel state from the bootloader handoff and optional state blob. +pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const BootInfo) { + let boot_info = unsafe { boot_info_ptr.as_ref() }; + if let Some(info) = init_boot_info(boot_info) { + unsafe { + page_allocator::init(info); + heap::init(info.heap_ptr, info.va_base, info.va_len); + } + trap::init_trap_vector(info.kstack_top); + init_state(state_ptr, state_len); + } else { + panic!("init_kernel: missing boot info"); + } + log!("kernel initialized"); +} + +fn init_state(state_ptr: *const u8, state_len: usize) { + unsafe { + let state_slot = STATE.get_mut(); + if !state_ptr.is_null() && state_len > 0 { + let bytes = slice::from_raw_parts(state_ptr, state_len); + *state_slot = State::decode(bytes).or_else(|| { + log!("state decode failed; starting empty state"); + Some(State::new()) + }); + if state_slot.is_some() { + logf!("state initialized (len=%d)", state_len as u32); + } + } else { + *state_slot = Some(State::new()); + } + } +} + +pub(crate) fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { + logf!( + "init_boot_info: boot_info_ptr=0x%x", + boot_info + .map(|info| info as *const BootInfo as usize as u32) + .unwrap_or(0) + ); + if let Some(info) = boot_info { + let task = Task::kernel( + info.root_ppn, + info.heap_ptr, + info.va_base, + info.va_len, + ); + unsafe { + let tasks_slot = TASKS.get_mut(); + if tasks_slot.set_at(KERNEL_TASK_SLOT, task).is_err() { + log!("kernel task slot unavailable; kernel task not recorded"); + } + *CURRENT_TASK.get_mut() = KERNEL_TASK_SLOT; + } + logf!( + "boot_info: root_ppn=0x%x kstack_top=0x%x heap_ptr=0x%x mem_size=%d", + info.root_ppn, + info.kstack_top, + info.heap_ptr, + info.memory_size + ); + Some(info) + } else { + log!("boot_info missing; kernel task not initialized"); + None + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs new file mode 100644 index 0000000..c084723 --- /dev/null +++ b/crates/kernel/src/lib.rs @@ -0,0 +1,41 @@ +#![no_std] +#![feature(naked_functions)] +#![feature(alloc_error_handler)] + +pub use types::boot::BootInfo; +pub mod global; +pub mod task; +pub use task::{AddressSpace, Task, TrapFrame}; +pub use task::{ + kernel_run_task, + prep_program_task, + run_task, + PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES, +}; +pub mod memory; +pub mod trap; +pub mod syscall; +pub mod user_program; + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let mut buf = [0u8; 256]; + let len = { + let mut writer = clibc::BufferWriter::new(&mut buf); + if write!(&mut writer, "{}", info).is_ok() { + writer.len() + } else { + 0 + } + }; + if len == 0 { + clibc::log!("kernel panic"); + } else { + clibc::logf!("kernel panic: %s", buf.as_ptr() as u32, len as u32); + } + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs new file mode 100644 index 0000000..b0b82c1 --- /dev/null +++ b/crates/kernel/src/main.rs @@ -0,0 +1,49 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +use core::slice; +use kernel::BootInfo; +use clibc::{log, logf}; + +mod init; +mod bundle; +use crate::bundle::{decode_bundle, process_bundle}; +use crate::init::init_kernel; + +#[allow(dead_code)] +const KERNEL_TASK_IDX: usize = 0; + +/// Kernel entrypoint. Receives: +/// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. +/// - `state_ptr`/`state_len`: optional state blob (currently unused). +/// - `boot_info_ptr`: bootloader handoff with stack + page-table root info. +#[unsafe(no_mangle)] +pub extern "C" fn _start( + bundle_ptr: *const u8, + bundle_len: usize, + state_ptr: *const u8, + state_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel boot"); + + init_kernel(state_ptr, state_len, boot_info_ptr); + + let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; + if decode_bundle(encoded_bundle) { + process_bundle(); + } else { + log!("bundle decode failed"); + } + + log!("finished bundle execution"); + halt(); +} + +#[inline(never)] +fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/memory/heap.rs b/crates/kernel/src/memory/heap.rs new file mode 100644 index 0000000..ad68dc2 --- /dev/null +++ b/crates/kernel/src/memory/heap.rs @@ -0,0 +1,82 @@ +use crate::global::Global; +use core::alloc::{GlobalAlloc, Layout}; +use core::ptr; + +#[derive(Clone, Copy)] +pub(crate) struct BumpAllocator { + next: usize, + end: usize, +} + +impl BumpAllocator { + pub(crate) const fn empty() -> Self { + Self { next: 0, end: 0 } + } + + fn init(&mut self, start: usize, end: usize) { + self.next = start; + self.end = end; + } + + fn alloc(&mut self, size: usize, align: usize) -> Option<*mut u8> { + if size == 0 || align == 0 || (align & (align - 1)) != 0 { + return None; + } + let start = align_up(self.next, align)?; + let end = start.checked_add(size)?; + if end > self.end { + return None; + } + self.next = end; + Some(start as *mut u8) + } +} + +/// Initialize the kernel bump allocator using the bootloader-provided heap pointer +/// and the mapped kernel VA window. +pub fn init(heap_ptr: u32, va_base: u32, va_len: u32) { + let start = heap_ptr as usize; + let end = (va_base as usize).saturating_add(va_len as usize); + unsafe { + crate::global::KERNEL_HEAP.get_mut().init(start, end); + } +} + +/// Allocate a kernel buffer from the bump allocator. +/// +/// Returns a kernel virtual address on success, or None on exhaustion/invalid args. +pub fn alloc(size: usize, align: usize) -> Option<*mut u8> { + unsafe { + crate::global::KERNEL_HEAP.get_mut().alloc(size, align) + } +} + +/// Deallocate a kernel buffer. Bump allocator does not reclaim memory yet. +pub fn dealloc(_ptr: *mut u8, _size: usize, _align: usize) {} + +fn align_up(value: usize, align: usize) -> Option { + let mask = align - 1; + value.checked_add(mask).map(|v| v & !mask) +} + +struct KernelAlloc; + +unsafe impl GlobalAlloc for KernelAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + alloc(layout.size(), layout.align()).unwrap_or(ptr::null_mut()) + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +#[global_allocator] +static KERNEL_ALLOC: KernelAlloc = KernelAlloc; + +#[alloc_error_handler] +fn alloc_error(layout: Layout) -> ! { + panic!( + "kernel alloc error: size={} align={}", + layout.size(), + layout.align() + ); +} diff --git a/crates/kernel/src/memory/mod.rs b/crates/kernel/src/memory/mod.rs new file mode 100644 index 0000000..dd79fae --- /dev/null +++ b/crates/kernel/src/memory/mod.rs @@ -0,0 +1,2 @@ +pub mod heap; +pub mod page_allocator; diff --git a/crates/kernel/src/memory/page_allocator.rs b/crates/kernel/src/memory/page_allocator.rs new file mode 100644 index 0000000..80765bc --- /dev/null +++ b/crates/kernel/src/memory/page_allocator.rs @@ -0,0 +1,489 @@ +use core::{cmp, marker::PhantomData, ptr}; + +use crate::global::{PAGE_ALLOC, ROOT_PPN}; +use crate::BootInfo; +use types::{ + Sv32PagePerms, Sv32PageTable, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE, SV32_VPN_MASK, map_allocating, + SV32_PTE_R, SV32_PTE_W, SV32_PTE_X, SV32_PTE_V, SV32_PTE_U, map_to_physical, +}; + +const PAGE_SIZE: usize = SV32_PAGE_SIZE; +const DIRECT_MAP_BASE: usize = SV32_DIRECT_MAP_BASE as usize; + +/// Permissions used by the kernel/user mapping helpers. +pub type PagePerms = Sv32PagePerms; + +#[derive(Debug, Clone, Copy)] +pub struct PageAllocator { + next_ppn: u32, + limit_ppn: u32, +} + +impl PageAllocator { + /// Create a bump-frame allocator over [start_ppn, limit_ppn). + pub const fn new(start_ppn: u32, limit_ppn: u32) -> Self { + Self { + next_ppn: start_ppn, + limit_ppn, + } + } + + /// Allocate the next free physical page number, or None if exhausted. + pub fn alloc(&mut self) -> Option { + if self.next_ppn >= self.limit_ppn { + return None; + } + let ppn = self.next_ppn; + self.next_ppn += 1; + Some(ppn) + } + + /// Zero a 4 KiB page in guest physical memory via the direct map. + fn zero_page(ppn: u32) { + let base = (ppn as usize) + .checked_mul(PAGE_SIZE) + .expect("page offset overflow"); + let virt = direct_map_addr(base).expect("direct map overflow while zeroing page"); + unsafe { + ptr::write_bytes(virt as *mut u8, 0, PAGE_SIZE); + } + } + + /// Advance the allocator so it will not hand out frames below `min_ppn`. + pub fn bump_to(&mut self, min_ppn: u32) { + if self.next_ppn < min_ppn { + self.next_ppn = min_ppn; + } + } + + pub fn limit_ppn(&self) -> u32 { + self.limit_ppn + } + + pub fn next_ppn(&self) -> u32 { + self.next_ppn + } + + pub fn remaining_ppn(&self) -> u32 { + self.limit_ppn.saturating_sub(self.next_ppn) + } +} + +/// Return the kernel's current root PPN (satp PPN field). +pub fn current_root() -> u32 { + unsafe { *ROOT_PPN.get_mut() } +} + +/// Update the current root PPN used by kernel helpers. +pub fn set_current_root(root_ppn: u32) { + unsafe { + *ROOT_PPN.get_mut() = root_ppn; + } +} + +/// Initialize the kernel MMU allocator state from bootloader handoff. +pub fn init(boot_info: &BootInfo) { + unsafe { + *ROOT_PPN.get_mut() = boot_info.root_ppn; + let limit_ppn = (boot_info.memory_size as usize / PAGE_SIZE) as u32; + *PAGE_ALLOC.get_mut() = Some(PageAllocator::new(boot_info.next_free_ppn, limit_ppn)); + } +} + +/// Allocate and zero a fresh L1 root page table. Returns None if out of frames. +pub fn alloc_root() -> Option { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let root = alloc.alloc()?; + PageAllocator::zero_page(root); + Some(root) + } + None => None, + } +} + +/// Ensure the page allocator will not hand out frames below `min_ppn`. +pub fn bump_page_allocator(min_ppn: u32) { + unsafe { + if let Some(alloc) = PAGE_ALLOC.get_mut() { + alloc.bump_to(min_ppn); + } + } +} + +pub fn total_ppn() -> Option { + unsafe { PAGE_ALLOC.get_mut().as_ref().map(|alloc| alloc.limit_ppn()) } +} + +/// Map a user-visible virtual range with the provided permissions into a specific root. +pub fn map_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + if len == 0 { + return true; + } + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let page_size = PAGE_SIZE; + let start = align_down_local(va_start as usize, page_size); + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up_local(v, page_size), + None => return false, + }; + let page_count = (end - start) / page_size; + let vpn1_start = ((start as u32) >> 22) & SV32_VPN_MASK; + let vpn1_end = (((end - 1) as u32) >> 22) & SV32_VPN_MASK; + let l2_tables = vpn1_end + .checked_sub(vpn1_start) + .map(|v| v as usize + 1) + .unwrap_or(0); + let needed = page_count + l2_tables; + let available = alloc.remaining_ppn() as usize; + if needed > available { + panic!( + "map_range_for_root: out of physical memory (need {} pages, have {})", + needed, available + ); + } + let mapper = KernelMapper::new(alloc); + map_allocating(&mapper, root_ppn, va_start, len, perms) + } + None => false, + } +} + +/// Map a user-visible virtual range with the provided permissions into the current root. +pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { + let root = unsafe { *ROOT_PPN.get_mut() }; + map_range_for_root(root, va_start, len, perms) +} + +/// Map a kernel-only virtual range with the provided permissions into a specific root. +pub fn map_kernel_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let mapper = KernelMapper::new(alloc); + map_allocating(&mapper, root_ppn, va_start, len, perms) + } + None => false, + } +} + +/// Map a kernel-only virtual range with the provided permissions into the current root. +pub fn map_kernel_range(va_start: u32, len: usize, perms: PagePerms) -> bool { + let root = unsafe { *ROOT_PPN.get_mut() }; + map_kernel_range_for_root(root, va_start, len, perms) +} + +/// Map a VA range in `root_ppn` to an explicit physical range (no allocation). +pub fn map_physical_range_for_root( + root_ppn: u32, + va_start: u32, + phys_start: u32, + len: usize, + perms: PagePerms, +) -> bool { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let mapper = KernelMapper::new(alloc); + map_to_physical(&mapper, root_ppn, va_start, phys_start, len, perms) + } + None => false, + } +} + +/// Mirror a mapped user range from `user_root` into the current kernel root so the +/// kernel can execute the user program without switching satp. +pub fn mirror_user_range_into_kernel(user_root: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + if len == 0 { + return true; + } + let page_size = PAGE_SIZE; + let start = align_down_local(va_start as usize, page_size) as u32; + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up_local(v, page_size) as u32, + None => return false, + }; + let kernel_root = current_root(); + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + let mapper_alloc = match alloc { + Some(a) => a, + None => return false, + }; + let mut va = start; + while va < end { + let phys = match translate(user_root, va) { + Some(p) => p as u32, + None => return false, + }; + if !overwrite_map_page(kernel_root, va, phys, perms, mapper_alloc) { + return false; + } + va = va.wrapping_add(page_size as u32); + } + true +} + +/// Walk Sv32 to translate a VA in the given root to a physical address. +pub fn translate(root_ppn: u32, va: u32) -> Option { + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + let offset = (va & 0xfff) as usize; + + let l1_base = (root_ppn as usize) + .checked_mul(PAGE_SIZE)?; + let l1_addr = l1_base + vpn1 as usize * core::mem::size_of::(); + let l1_pte = read_pte(l1_addr)?; + if l1_pte & SV32_PTE_V == 0 || l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return None; + } + + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(PAGE_SIZE)?; + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + let l2_pte = read_pte(l2_addr)?; + if l2_pte & SV32_PTE_V == 0 { + return None; + } + + let ppn = (l2_pte >> 10) as usize; + ppn.checked_mul(PAGE_SIZE)?.checked_add(offset) +} + +fn leaf_pte(root_ppn: u32, va: u32) -> Option { + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let l1_base = (root_ppn as usize) + .checked_mul(PAGE_SIZE)?; + let l1_addr = l1_base + vpn1 as usize * core::mem::size_of::(); + let l1_pte = read_pte(l1_addr)?; + if l1_pte & SV32_PTE_V == 0 || l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return None; + } + + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(PAGE_SIZE)?; + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + let l2_pte = read_pte(l2_addr)?; + if l2_pte & SV32_PTE_V == 0 { + return None; + } + Some(l2_pte) +} + +/// Peek a 32-bit value at a VA in a given root using the direct-map window. +pub fn peek_word(root_ppn: u32, va: u32) -> Option { + let phys = translate(root_ppn, va)?; + let va_ptr = direct_map_addr(phys)?; + Some(unsafe { (va_ptr as *const u32).read_volatile() }) +} + +/// Copy data into a user VA range for a specific root using the direct-map window. +pub fn copy(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { + if data.is_empty() { + return true; + } + let mut remaining = data.len(); + let mut src_off = 0usize; + let mut va = va_start; + while remaining > 0 { + let phys = match translate(root_ppn, va) { + Some(p) => p, + None => return false, + }; + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + let dst = match direct_map_addr(phys) { + Some(v) => v, + None => return false, + }; + unsafe { + ptr::copy_nonoverlapping( + data.as_ptr().add(src_off), + dst as *mut u8, + to_copy, + ); + } + remaining -= to_copy; + src_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + true +} + +/// Copy data into a user VA range atomically, failing if any page is not user-writable. +pub fn copy_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { + if data.is_empty() { + return true; + } + let mut remaining = data.len(); + let mut va = va_start; + while remaining > 0 { + let pte = match leaf_pte(root_ppn, va) { + Some(p) => p, + None => return false, + }; + let is_user = (pte & SV32_PTE_U) != 0; + let can_write = (pte & SV32_PTE_W) != 0; + if !is_user || !can_write { + return false; + } + let phys = match translate(root_ppn, va) { + Some(p) => p, + None => return false, + }; + if direct_map_addr(phys).is_none() { + return false; + } + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + remaining -= to_copy; + va = va.wrapping_add(to_copy as u32); + } + + let mut remaining = data.len(); + let mut src_off = 0usize; + let mut va = va_start; + while remaining > 0 { + let phys = match translate(root_ppn, va) { + Some(p) => p, + None => return false, + }; + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + let dst = match direct_map_addr(phys) { + Some(v) => v, + None => return false, + }; + unsafe { + ptr::copy_nonoverlapping( + data.as_ptr().add(src_off), + dst as *mut u8, + to_copy, + ); + } + remaining -= to_copy; + src_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + true +} + +/// Sv32 page-table accessor that routes PTE traffic through the kernel's direct map. +struct KernelMapper<'a> { + alloc: *mut PageAllocator, + _marker: PhantomData<&'a mut PageAllocator>, +} + +impl<'a> KernelMapper<'a> { + fn new(alloc: &'a mut PageAllocator) -> Self { + Self { + alloc: alloc as *mut PageAllocator, + _marker: PhantomData, + } + } +} + +impl<'a> Sv32PageTable for KernelMapper<'a> { + fn page_size(&self) -> usize { + PAGE_SIZE + } + + fn read_pte(&self, phys_addr: usize) -> Option { + let va = direct_map_addr(phys_addr)?; + Some(unsafe { (va as *const u32).read_volatile() }) + } + + fn write_pte(&self, phys_addr: usize, val: u32) { + if let Some(va) = direct_map_addr(phys_addr) { + unsafe { (va as *mut u32).write_volatile(val) }; + } + } + + fn alloc_frame(&self) -> Option { + let alloc = unsafe { &mut *self.alloc }; + alloc.alloc() + } + + fn zero_frame(&self, ppn: u32) { + PageAllocator::zero_page(ppn); + } +} + +fn direct_map_addr(phys: usize) -> Option { + DIRECT_MAP_BASE.checked_add(phys) +} + +fn read_pte(phys_addr: usize) -> Option { + let va = direct_map_addr(phys_addr)?; + Some(unsafe { (va as *const u32).read_volatile() }) +} + +fn write_pte(phys_addr: usize, val: u32) { + if let Some(va) = direct_map_addr(phys_addr) { + unsafe { (va as *mut u32).write_volatile(val) }; + } +} + +const fn align_up_local(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +const fn align_down_local(val: usize, align: usize) -> usize { + val & !(align - 1) +} + +fn overwrite_map_page( + root_ppn: u32, + va: u32, + phys_start: u32, + perms: PagePerms, + alloc: &mut PageAllocator, +) -> bool { + let page_size = PAGE_SIZE; + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let root_base = (root_ppn as usize) + .checked_mul(page_size) + .unwrap(); + let l1_addr = root_base + vpn1 as usize * core::mem::size_of::(); + let mut l1_pte = read_pte(l1_addr).unwrap_or(0); + if l1_pte & SV32_PTE_V == 0 { + let l2 = match alloc.alloc() { + Some(ppn) => ppn, + None => return false, + }; + PageAllocator::zero_page(l2); + l1_pte = (l2 << 10) | SV32_PTE_V; + write_pte(l1_addr, l1_pte); + } else if l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return false; + } + + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(page_size) + .unwrap(); + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + + let leaf_ppn = phys_start / page_size as u32; + let mut flags = SV32_PTE_V; + if perms.read { + flags |= SV32_PTE_R; + } + if perms.write { + flags |= SV32_PTE_W; + } + if perms.exec { + flags |= SV32_PTE_X; + } + if perms.user { + flags |= types::SV32_PTE_U; + } + let leaf = (leaf_ppn << 10) | flags; + write_pte(l2_addr, leaf); + true +} diff --git a/crates/kernel/src/memory/tests/first_test.rs b/crates/kernel/src/memory/tests/first_test.rs new file mode 100644 index 0000000..6ae5036 --- /dev/null +++ b/crates/kernel/src/memory/tests/first_test.rs @@ -0,0 +1,30 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Basic smoke test: init kernel test harness and emit logs. +use core::slice; +use clibc::log; +use kernel::BootInfo; + +#[path = "../../tests/utils.rs"] +mod utils; +#[path = "../../tests/results.rs"] +mod results; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel test boot"); + let _info = utils::init_test_kernel(boot_info_ptr); + + let input = unsafe { slice::from_raw_parts(input_ptr, input_len) }; + clibc::logf!("kernel test input len: %d", input.len() as u32); + log!("kernel test log-only"); + + utils::pass(); +} diff --git a/crates/kernel/src/memory/tests/heap_edge_test.rs b/crates/kernel/src/memory/tests/heap_edge_test.rs new file mode 100644 index 0000000..7c7c529 --- /dev/null +++ b/crates/kernel/src/memory/tests/heap_edge_test.rs @@ -0,0 +1,91 @@ +#![no_std] +#![no_main] + +// Heap edge tests: invalid layouts and monotonic bump behavior. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::heap; + +#[path = "../../tests/results.rs"] +mod results; +#[path = "../../tests/utils.rs"] +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel heap edge test boot"); + let _info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_invalid_layouts() { + utils::fail(code); + } + if let Err(code) = test_monotonic_bump_and_data() { + utils::fail(code); + } + + log!("kernel heap edge test done"); + utils::pass(); +} + +fn test_invalid_layouts() -> Result<(), u32> { + // Description: heap::alloc must reject invalid size/align values. + log!("test: invalid heap layouts are rejected"); + log!("subtest: zero size and zero align are rejected"); + + if heap::alloc(0, 8).is_some() { + return Err(10); + } + if heap::alloc(16, 0).is_some() { + return Err(11); + } + + log!("subtest: non power-of-two alignment is rejected"); + if heap::alloc(16, 3).is_some() { + return Err(12); + } + + log!("subtest: size overflow is rejected"); + if heap::alloc(usize::MAX, 16).is_some() { + return Err(13); + } + Ok(()) +} + +fn test_monotonic_bump_and_data() -> Result<(), u32> { + // Description: allocations should be monotonic and memory is usable. + log!("test: monotonic bump allocator behavior"); + log!("subtest: allocations are ordered and writable"); + + let a = heap::alloc(32, 8).unwrap_or(core::ptr::null_mut()); + let b = heap::alloc(32, 8).unwrap_or(core::ptr::null_mut()); + if a.is_null() || b.is_null() { + return Err(20); + } + if (b as usize) <= (a as usize) { + return Err(21); + } + unsafe { + a.write_bytes(0xab, 32); + b.write_bytes(0xcd, 32); + } + let a_first = unsafe { a.read() }; + let b_first = unsafe { b.read() }; + if a_first != 0xab || b_first != 0xcd { + return Err(22); + } + + log!("subtest: dealloc is a no-op and allocations keep increasing"); + heap::dealloc(a, 32, 8); + let c = heap::alloc(16, 8).unwrap_or(core::ptr::null_mut()); + if c.is_null() || (c as usize) <= (b as usize) { + return Err(23); + } + Ok(()) +} diff --git a/crates/kernel/src/memory/tests/mem_alloc_test.rs b/crates/kernel/src/memory/tests/mem_alloc_test.rs new file mode 100644 index 0000000..eb6b8c7 --- /dev/null +++ b/crates/kernel/src/memory/tests/mem_alloc_test.rs @@ -0,0 +1,79 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory allocation test: heap alignment, heap window exhaustion, page allocator roots. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::{heap, page_allocator}; + +#[path = "../../tests/results.rs"] +mod results; +#[path = "../../tests/utils.rs"] +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem alloc test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_heap_alignment() { + utils::fail(code); + } + if let Err(code) = test_heap_exhaustion(info) { + utils::fail(code); + } + if let Err(code) = test_page_allocator_roots() { + utils::fail(code); + } + if let Err(code) = test_heap_too_large() { + utils::fail(code); + } + + utils::pass(); +} + +fn test_heap_alignment() -> Result<(), u32> { + let ptr = heap::alloc(32, 16).unwrap_or(core::ptr::null_mut()); + if ptr.is_null() { + return Err(1); + } + if (ptr as usize) & 0x0f != 0 { + return Err(2); + } + heap::dealloc(ptr, 32, 16); + Ok(()) +} + +fn test_heap_exhaustion(info: BootInfo) -> Result<(), u32> { + let window_end = info.va_base.saturating_add(info.va_len) as usize; + let available = window_end.saturating_sub(info.heap_ptr as usize); + if heap::alloc(available.saturating_add(16), 8).is_some() { + return Err(3); + } + Ok(()) +} + +fn test_page_allocator_roots() -> Result<(), u32> { + let root1 = page_allocator::alloc_root().unwrap_or(0); + let root2 = page_allocator::alloc_root().unwrap_or(0); + if root1 == 0 || root2 == 0 || root1 == root2 { + return Err(4); + } + Ok(()) +} + +fn test_heap_too_large() -> Result<(), u32> { + if heap::alloc(usize::MAX, 8).is_some() { + return Err(5); + } + Ok(()) +} diff --git a/crates/kernel/src/memory/tests/mem_map_edge_test.rs b/crates/kernel/src/memory/tests/mem_map_edge_test.rs new file mode 100644 index 0000000..e2c3486 --- /dev/null +++ b/crates/kernel/src/memory/tests/mem_map_edge_test.rs @@ -0,0 +1,443 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory mapping edge-case tests: alignment, boundary spanning, map_to_physical, mirror gaps, +// copy_user atomicity, and remap overrides. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator::{self, PagePerms}; + +#[path = "../../tests/results.rs"] +mod results; +#[path = "../../tests/utils.rs"] +mod utils; + +const PAGE_SIZE: usize = 0x1000; +const L1_SPAN: u32 = 1 << 22; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem map edge test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + let user_root = page_allocator::alloc_root().unwrap_or(0); + if user_root == 0 { + utils::fail(1); + } + + if let Err(code) = test_unaligned_map_and_translate(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_cross_l1_boundary(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_multiple_l2_tables(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_zero_len_map_no_effect(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_map_to_physical_alignment_and_alias(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_mirror_gap_behavior(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_copy_user_atomic(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_remap_override_perms(user_root, info) { + utils::fail(code); + } + + log!("kernel mem map edge test done"); + utils::pass(); +} + +fn test_unaligned_map_and_translate(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map an unaligned range that crosses a page boundary and verify translations + // and data access on the mapped pages. + log!("test: unaligned map + translate"); + log!("subtest: map unaligned range and confirm translations are present"); + + let base = pick_user_va(info, 0x4000); + let va_start = base.wrapping_add(37); + let len = PAGE_SIZE + 123; + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va_start, len, perms) { + return Err(10); + } + + let first_phys = page_allocator::translate(user_root, va_start).unwrap_or(0); + let last_phys = page_allocator::translate(user_root, va_start.wrapping_add(len as u32 - 1)) + .unwrap_or(0); + if first_phys == 0 || last_phys == 0 { + return Err(11); + } + + log!("subtest: write data and confirm it can be read back"); + let data = [0x12u8, 0x34, 0x56, 0x78]; + if !page_allocator::copy(user_root, va_start, &data) { + return Err(12); + } + let word = page_allocator::peek_word(user_root, va_start).unwrap_or(0); + if word != 0x7856_3412 { + return Err(13); + } + Ok(()) +} + +fn test_cross_l1_boundary(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map a range that crosses a VPN1 boundary and ensure both pages are mapped + // and writable. + log!("test: cross L1 boundary mapping"); + log!("subtest: pick a range that crosses a 4 MiB boundary"); + + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let next_boundary = align_up_u32(window_start.saturating_add(0x1000), L1_SPAN); + let start = next_boundary.saturating_sub(PAGE_SIZE as u32); + let end = start.saturating_add((PAGE_SIZE * 2) as u32); + if start < window_start || end > window_end { + log!("subtest: skipped (window too small for boundary test)"); + return Ok(()); + } + + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, start, PAGE_SIZE * 2, perms) { + return Err(20); + } + + log!("subtest: confirm translations and data access on both pages"); + let first_phys = page_allocator::translate(user_root, start).unwrap_or(0); + let second_phys = page_allocator::translate(user_root, start.wrapping_add(PAGE_SIZE as u32)) + .unwrap_or(0); + if first_phys == 0 || second_phys == 0 { + return Err(21); + } + + let first_data = [0xa1u8, 0xa2, 0xa3, 0xa4]; + let second_data = [0xb1u8, 0xb2, 0xb3, 0xb4]; + if !page_allocator::copy_user(user_root, start, &first_data) { + return Err(22); + } + if !page_allocator::copy_user(user_root, start.wrapping_add(PAGE_SIZE as u32), &second_data) { + return Err(23); + } + let first_word = page_allocator::peek_word(user_root, start).unwrap_or(0); + let second_word = page_allocator::peek_word(user_root, start.wrapping_add(PAGE_SIZE as u32)) + .unwrap_or(0); + if first_word != 0xa4a3_a2a1 || second_word != 0xb4b3_b2b1 { + return Err(24); + } + Ok(()) +} + +fn test_zero_len_map_no_effect(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: mapping a zero-length range should be a no-op. + log!("test: zero-length map is a no-op"); + log!("subtest: ensure translation stays absent"); + + let va = pick_user_va(info, 0x9000); + if page_allocator::translate(user_root, va).is_some() { + return Err(30); + } + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va, 0, perms) { + return Err(31); + } + if page_allocator::translate(user_root, va).is_some() { + return Err(32); + } + Ok(()) +} + +fn test_multiple_l2_tables(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map pages in multiple VPN1 regions to ensure more than two L2 tables + // can be allocated and accessed. + log!("test: multiple L2 tables via sparse VPN1 mapping"); + log!("subtest: map one page in three distinct VPN1 regions"); + + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let first_region = align_up_u32(window_start.saturating_add(0x1000), L1_SPAN); + let third_region = first_region.saturating_add(L1_SPAN * 2); + if third_region.saturating_add(PAGE_SIZE as u32) > window_end { + log!("subtest: skipped (window too small for multi-L2 test)"); + return Ok(()); + } + + let perms = PagePerms::new(true, true, false, true); + let mut vas = [0u32; 3]; + for (idx, va) in vas.iter_mut().enumerate() { + let region = first_region.saturating_add(L1_SPAN * idx as u32); + *va = align_down_u32(region, PAGE_SIZE as u32); + if !page_allocator::map_range_for_root(user_root, *va, PAGE_SIZE, perms) { + return Err(34); + } + } + + log!("subtest: write data and verify translations across each region"); + for (idx, va) in vas.iter().enumerate() { + let phys = page_allocator::translate(user_root, *va).unwrap_or(0); + if phys == 0 { + return Err(35); + } + let data = [0x90u8 + idx as u8, 0x91 + idx as u8, 0x92 + idx as u8, 0x93 + idx as u8]; + if !page_allocator::copy_user(user_root, *va, &data) { + return Err(36); + } + let word = page_allocator::peek_word(user_root, *va).unwrap_or(0); + let expected = (0x93u32 + idx as u32) << 24 + | (0x92u32 + idx as u32) << 16 + | (0x91u32 + idx as u32) << 8 + | (0x90u32 + idx as u32); + if word != expected { + return Err(37); + } + } + Ok(()) +} + +fn test_map_to_physical_alignment_and_alias( + user_root: u32, + info: BootInfo, +) -> Result<(), u32> { + // Description: map_to_physical must reject unaligned physical addresses and alias + // when aligned. + log!("test: map_to_physical alignment + aliasing"); + log!("subtest: create a source mapping to obtain a physical page"); + + let source_va = align_down_u32(pick_user_va(info, 0x12000), PAGE_SIZE as u32); + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, source_va, PAGE_SIZE, perms) { + return Err(40); + } + let source_phys = page_allocator::translate(user_root, source_va).unwrap_or(0); + if source_phys == 0 || source_phys % PAGE_SIZE != 0 { + return Err(41); + } + + log!("subtest: unaligned map_to_physical is rejected"); + let target_va = align_down_u32(pick_user_va(info, 0x18000), PAGE_SIZE as u32); + if page_allocator::map_physical_range_for_root( + user_root, + target_va, + (source_phys as u32).wrapping_add(1), + PAGE_SIZE, + perms, + ) { + return Err(42); + } + if page_allocator::translate(user_root, target_va).is_some() { + return Err(43); + } + + log!("subtest: aligned map_to_physical creates an alias"); + if !page_allocator::map_physical_range_for_root( + user_root, + target_va, + source_phys as u32, + PAGE_SIZE, + perms, + ) { + return Err(44); + } + let aliased = page_allocator::translate(user_root, target_va).unwrap_or(0); + if aliased != source_phys { + return Err(45); + } + + let data = [0x0cu8, 0x0d, 0x0e, 0x0f]; + if !page_allocator::copy_user(user_root, target_va, &data) { + return Err(46); + } + let word = page_allocator::peek_word(user_root, source_va).unwrap_or(0); + if word != 0x0f0e_0d0c { + return Err(47); + } + Ok(()) +} + +fn test_mirror_gap_behavior(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: mirroring a range with an unmapped page should fail and only mirror + // pages visited before the gap. + log!("test: mirror range with a gap"); + log!("subtest: create two mapped pages with an unmapped gap"); + + let base = align_down_u32(pick_user_va(info, 0x1e000), PAGE_SIZE as u32); + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, base, PAGE_SIZE, perms) { + return Err(50); + } + if !page_allocator::map_range_for_root( + user_root, + base.wrapping_add((PAGE_SIZE * 2) as u32), + PAGE_SIZE, + perms, + ) { + return Err(51); + } + + log!("subtest: mirror across the gap and verify partial mirroring"); + let kernel_root = page_allocator::current_root(); + let gap_va = base.wrapping_add(PAGE_SIZE as u32); + let end_va = base.wrapping_add((PAGE_SIZE * 2) as u32); + let kernel_before_gap = page_allocator::translate(kernel_root, gap_va); + let kernel_before_end = page_allocator::translate(kernel_root, end_va); + let mirror_ok = page_allocator::mirror_user_range_into_kernel( + user_root, + base, + PAGE_SIZE * 3, + perms, + ); + if mirror_ok { + return Err(52); + } + let user_phys = page_allocator::translate(user_root, base).unwrap_or(0); + let kernel_phys = page_allocator::translate(kernel_root, base).unwrap_or(0); + if user_phys == 0 || kernel_phys != user_phys { + return Err(53); + } + if page_allocator::translate(user_root, gap_va).is_some() { + return Err(54); + } + let kernel_after_gap = page_allocator::translate(kernel_root, gap_va); + if kernel_after_gap != kernel_before_gap { + return Err(55); + } + let kernel_after_end = page_allocator::translate(kernel_root, end_va); + if kernel_after_end != kernel_before_end { + return Err(56); + } + Ok(()) +} + +fn test_copy_user_atomic(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: copy_user should be atomic (all-or-nothing) across page boundaries. + log!("test: copy_user atomicity"); + log!("subtest: create writable + read-only pages"); + + let base = align_down_u32(pick_user_va(info, 0x26000), PAGE_SIZE as u32); + let perms_rw = PagePerms::new(true, true, false, true); + let perms_ro = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, base, PAGE_SIZE, perms_rw) { + return Err(60); + } + if !page_allocator::map_range_for_root( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + PAGE_SIZE, + perms_ro, + ) { + return Err(61); + } + + log!("subtest: seed data on both pages and near the boundary"); + let seed_first = [0x11u8, 0x22, 0x33, 0x44]; + let seed_second = [0xaau8, 0xbb, 0xcc, 0xdd]; + if !page_allocator::copy(user_root, base, &seed_first) { + return Err(62); + } + if !page_allocator::copy( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + &seed_second, + ) { + return Err(63); + } + let boundary_seed = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let boundary_start = base.wrapping_add(PAGE_SIZE as u32 - 8); + if !page_allocator::copy(user_root, boundary_start, &boundary_seed) { + return Err(64); + } + + log!("subtest: attempt a cross-page write and confirm no bytes changed"); + let mut big = [0u8; 16]; + for (i, byte) in big.iter_mut().enumerate() { + *byte = (0x80 + i as u8) as u8; + } + let cross_start = boundary_start; + if page_allocator::copy_user(user_root, cross_start, &big) { + return Err(65); + } + let first_word = page_allocator::peek_word(user_root, base).unwrap_or(0); + let boundary_word0 = page_allocator::peek_word(user_root, boundary_start).unwrap_or(0); + let boundary_word1 = page_allocator::peek_word( + user_root, + boundary_start.wrapping_add(4), + ) + .unwrap_or(0); + let second_word = page_allocator::peek_word( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + ) + .unwrap_or(0); + if first_word != 0x4433_2211 + || boundary_word0 != 0x0403_0201 + || boundary_word1 != 0x0807_0605 + || second_word != 0xddcc_bbaa + { + return Err(66); + } + Ok(()) +} + +fn test_remap_override_perms(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: remapping an existing VA should override permissions. + log!("test: remap overrides permissions"); + log!("subtest: map RW, write data, then remap RO"); + + let va = align_down_u32(pick_user_va(info, 0x2e000), PAGE_SIZE as u32); + let perms_rw = PagePerms::new(true, true, false, true); + let perms_ro = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, va, PAGE_SIZE, perms_rw) { + return Err(70); + } + let first = [0x0au8, 0x0b, 0x0c, 0x0d]; + if !page_allocator::copy_user(user_root, va, &first) { + return Err(71); + } + if !page_allocator::map_range_for_root(user_root, va, PAGE_SIZE, perms_ro) { + return Err(72); + } + + log!("subtest: verify write is rejected and existing data remains"); + let second = [0xf1u8, 0xf2, 0xf3, 0xf4]; + if page_allocator::copy_user(user_root, va, &second) { + return Err(73); + } + let word = page_allocator::peek_word(user_root, va).unwrap_or(0); + if word != 0x0d0c_0b0a { + return Err(74); + } + Ok(()) +} + +fn pick_user_va(info: BootInfo, offset: u32) -> u32 { + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let mut candidate = window_start.saturating_add(offset); + let len = PAGE_SIZE as u32 * 4; + if candidate.saturating_add(len) > window_end { + candidate = window_start.saturating_add(PAGE_SIZE as u32); + } + candidate +} + +const fn align_down_u32(val: u32, align: u32) -> u32 { + val & !(align - 1) +} + +const fn align_up_u32(val: u32, align: u32) -> u32 { + (val + (align - 1)) & !(align - 1) +} diff --git a/crates/kernel/src/memory/tests/mem_map_test.rs b/crates/kernel/src/memory/tests/mem_map_test.rs new file mode 100644 index 0000000..45e5bf1 --- /dev/null +++ b/crates/kernel/src/memory/tests/mem_map_test.rs @@ -0,0 +1,198 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory mapping test: user range mapping, kernel mirroring, translate/copy/peek. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator::{self, PagePerms}; + +#[path = "../../tests/results.rs"] +mod results; +#[path = "../../tests/utils.rs"] +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem map test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + let user_root = page_allocator::alloc_root().unwrap_or(0); + if user_root == 0 { + utils::fail(1); + } + + let (va_start, len) = pick_user_range(info); + + if let Err(code) = test_user_map(user_root, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_read_only_mapping(user_root, info, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_exec_mapping(user_root, info, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_kernel_sees_different_phys_before_mirror(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_mirror(user_root, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_translate(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_copy_peek(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_user_cannot_translate_kernel_only(user_root, info) { + utils::fail(code); + } + + log!("kernel mem map test done"); + utils::pass(); +} + +fn pick_user_range(info: BootInfo) -> (u32, usize) { + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let mut va_start = window_start.saturating_add(0x20_000); + let len = 0x1000usize; + if va_start.saturating_add(len as u32) > window_end { + va_start = window_start.saturating_add(0x1000); + } + (va_start, len) +} + +fn test_user_map(user_root: u32, va_start: u32, len: usize) -> Result<(), u32> { + // Map a user R/W range into the user root. + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va_start, len, perms) { + return Err(2); + } + // Verify the mapping exists and can be written via the user root. + let phys = page_allocator::translate(user_root, va_start).unwrap_or(0); + if phys == 0 { + return Err(12); + } + let data = [0x0bu8, 0x0c, 0x0d, 0x0e]; + if !page_allocator::copy_user(user_root, va_start, &data) { + return Err(13); + } + Ok(()) +} + +fn test_read_only_mapping( + user_root: u32, + info: BootInfo, + base_va: u32, + len: usize, +) -> Result<(), u32> { + // Map a user read-only range and ensure writes are rejected by copy_user. + let window_end = info.va_base.saturating_add(info.va_len); + let mut ro_va = base_va.saturating_add(len as u32).saturating_add(0x1000); + if ro_va.saturating_add(len as u32) > window_end { + ro_va = info.va_base.saturating_add(0x2000); + } + let perms = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, ro_va, len, perms) { + return Err(10); + } + let data = [0x5au8, 0xa5]; + if page_allocator::copy_user(user_root, ro_va, &data) { + return Err(11); + } + Ok(()) +} + +fn test_exec_mapping( + user_root: u32, + info: BootInfo, + base_va: u32, + len: usize, +) -> Result<(), u32> { + // Map a user exec-only range and ensure writes are rejected. + let window_end = info.va_base.saturating_add(info.va_len); + let mut exec_va = base_va.saturating_add(len as u32).saturating_add(0x2000); + if exec_va.saturating_add(len as u32) > window_end { + exec_va = info.va_base.saturating_add(0x3000); + } + let perms = PagePerms::new(false, false, true, true); + if !page_allocator::map_range_for_root(user_root, exec_va, len, perms) { + return Err(14); + } + let data = [0x7eu8, 0x7f]; + if page_allocator::copy_user(user_root, exec_va, &data) { + return Err(15); + } + Ok(()) +} + +fn test_mirror(user_root: u32, va_start: u32, len: usize) -> Result<(), u32> { + // Mirror the user range into the kernel root so it is accessible in kernel. + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::mirror_user_range_into_kernel(user_root, va_start, len, perms) { + return Err(3); + } + Ok(()) +} + +fn test_kernel_sees_different_phys_before_mirror(user_root: u32, va_start: u32) -> Result<(), u32> { + // Ensure kernel/user roots resolve the same VA to different physical pages pre-mirror. + let kernel_root = page_allocator::current_root(); + let phys_user = page_allocator::translate(user_root, va_start).unwrap_or(0); + let phys_kernel = page_allocator::translate(kernel_root, va_start).unwrap_or(0); + if phys_user == 0 || phys_kernel == 0 { + return Err(9); + } + if phys_user == phys_kernel { + return Err(9); + } + Ok(()) +} + +fn test_translate(user_root: u32, va_start: u32) -> Result<(), u32> { + // Ensure mirror caused kernel/user translations to resolve to the same physical page. + let kernel_root = page_allocator::current_root(); + let phys_user = page_allocator::translate(user_root, va_start).unwrap_or(0); + let phys_kernel = page_allocator::translate(kernel_root, va_start).unwrap_or(0); + if phys_user == 0 || phys_kernel == 0 || phys_user != phys_kernel { + return Err(4); + } + Ok(()) +} + +fn test_copy_peek(user_root: u32, va_start: u32) -> Result<(), u32> { + // Copy via user root and read via kernel root to verify shared physical mapping. + let kernel_root = page_allocator::current_root(); + let data = [0x11u8, 0x22, 0x33, 0x44]; + if !page_allocator::copy(user_root, va_start, &data) { + return Err(5); + } + let word = page_allocator::peek_word(kernel_root, va_start).unwrap_or(0); + if word != 0x4433_2211 { + return Err(6); + } + Ok(()) +} + +fn test_user_cannot_translate_kernel_only(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Map kernel-only memory and ensure user root cannot translate it. + let kernel_only_va = info.va_base.saturating_add(0x300_000); + let len = 0x1000usize; + if !page_allocator::map_kernel_range(kernel_only_va, len, PagePerms::kernel_rw()) { + return Err(7); + } + if page_allocator::translate(user_root, kernel_only_va).is_some() { + return Err(8); + } + Ok(()) +} diff --git a/crates/kernel/src/memory/tests/page_alloc_test.rs b/crates/kernel/src/memory/tests/page_alloc_test.rs new file mode 100644 index 0000000..de7a80f --- /dev/null +++ b/crates/kernel/src/memory/tests/page_alloc_test.rs @@ -0,0 +1,79 @@ +#![no_std] +#![no_main] + +// Page allocator tests: root zeroing and bump behavior. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator; + +#[path = "../../tests/results.rs"] +mod results; +#[path = "../../tests/utils.rs"] +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel page allocator test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_alloc_root_zeroed(info) { + utils::fail(code); + } + if let Err(code) = test_bump_allocator_behavior() { + utils::fail(code); + } + + log!("kernel page allocator test done"); + utils::pass(); +} + +fn test_alloc_root_zeroed(info: BootInfo) -> Result<(), u32> { + // Description: freshly allocated roots should be zeroed (no valid mappings). + log!("test: alloc_root yields zeroed page table"); + log!("subtest: ensure translation is absent for a fresh root"); + + let root = page_allocator::alloc_root().unwrap_or(0); + if root == 0 { + return Err(10); + } + let va = info.va_base.saturating_add(0x1000); + if page_allocator::translate(root, va).is_some() { + return Err(11); + } + Ok(()) +} + +fn test_bump_allocator_behavior() -> Result<(), u32> { + // Description: bumping should skip page frames and allow exhaustion testing. + log!("test: bump_page_allocator skips frames"); + log!("subtest: bump to a higher ppn and verify allocations skip ahead"); + + let first = page_allocator::alloc_root().unwrap_or(0); + if first == 0 { + return Err(20); + } + let bump_to = first.saturating_add(4); + page_allocator::bump_page_allocator(bump_to); + let second = page_allocator::alloc_root().unwrap_or(0); + if second < bump_to { + return Err(21); + } + + log!("subtest: bump to limit and verify allocator is exhausted"); + let limit = page_allocator::total_ppn().unwrap_or(0); + if limit == 0 { + return Err(22); + } + page_allocator::bump_page_allocator(limit); + if page_allocator::alloc_root().is_some() { + return Err(23); + } + Ok(()) +} diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs new file mode 100644 index 0000000..9f2094f --- /dev/null +++ b/crates/kernel/src/syscall/alloc.rs @@ -0,0 +1,81 @@ +use clibc::{log, logf}; + +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::Task; + +pub(crate) fn alloc_in_task(task: &mut Task, size: u32, align: u32) -> Option { + if size == 0 { + log!("sys_alloc: invalid size 0"); + return None; + } + if align == 0 || (align & (align - 1)) != 0 { + logf!("sys_alloc: invalid alignment %d", align); + return None; + } + + let mask = align - 1; + let start = match task.heap_ptr.checked_add(mask) { + Some(addr) => addr & !mask, + None => { + log!("sys_alloc: heap ptr overflow"); + return None; + } + }; + let end = match start.checked_add(size) { + Some(end) => end, + None => { + log!("sys_alloc: size overflow"); + return None; + } + }; + + let window_base = task.addr_space.va_base; + let window_limit = window_base.saturating_add(task.addr_space.va_len); + if start < window_base || end > window_limit { + logf!( + "sys_alloc: heap range exceeds task window start=0x%x end=0x%x window=[0x%x,0x%x)", + start, + end, + window_base, + window_limit + ); + return None; + } + task.heap_ptr = end; + Some(start) +} + +pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { + let size = args[0]; + let align = args[1]; + + let current = unsafe { *CURRENT_TASK.get_mut() }; + // Kernel task should never call sys_alloc. + if current == KERNEL_TASK_SLOT { + panic!("sys_alloc: kernel task cannot allocate memory"); + } + + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get_mut(current) { + Some(task) => task, + None => { + logf!("sys_alloc: no current task for slot %d", current as u32); + return 0; + } + }; + + match alloc_in_task(task, size, align) { + Some(addr) => addr, + None => 0, + } +} + +pub(crate) fn sys_dealloc(_args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + // Kernel task should never call sys_alloc. + if current == KERNEL_TASK_SLOT { + panic!("sys_alloc: kernel task cannot allocate memory"); + } + // No-op: kernel heap is bump-only for now. + 0 +} diff --git a/crates/kernel/src/syscall/balance.rs b/crates/kernel/src/syscall/balance.rs new file mode 100644 index 0000000..579b67c --- /dev/null +++ b/crates/kernel/src/syscall/balance.rs @@ -0,0 +1,92 @@ +use clibc::{log, logf}; +use types::{Address, ADDRESS_LEN}; + +use state::State; + +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE}; +use crate::memory::page_allocator as mmu; +use crate::syscall::alloc::sys_alloc; +use crate::syscall::storage::{current_task_root_ppn, read_user_bytes}; +use crate::global::FROM_PTR_ADDR; + +pub(crate) fn sys_transfer(args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + log!("sys_transfer: kernel task not allowed"); + return 1; + } + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 1, + }; + + let to_ptr = args[1]; + let value = (args[2] as u64) | ((args[3] as u64) << 32); + + let from_bytes = match read_user_bytes(root_ppn, FROM_PTR_ADDR, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 1, + }; + let to_bytes = match read_user_bytes(root_ppn, to_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 1, + }; + if from_bytes.len() != ADDRESS_LEN || to_bytes.len() != ADDRESS_LEN { + log!("sys_transfer: invalid address length"); + return 1; + } + + let mut from_buf = [0u8; ADDRESS_LEN]; + let mut to_buf = [0u8; ADDRESS_LEN]; + from_buf.copy_from_slice(&from_bytes); + to_buf.copy_from_slice(&to_bytes); + let from = Address(from_buf); + let to = Address(to_buf); + + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let ok = state.transfer(&from, &to, value); + if ok { 0 } else { 1 } +} + +pub(crate) fn sys_balance(args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + log!("sys_balance: kernel task not allowed"); + return 0; + } + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 0, + }; + let addr_ptr = args[0]; + let address_bytes = match read_user_bytes(root_ppn, addr_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_balance: invalid address length"); + return 0; + } + let mut addr_buf = [0u8; ADDRESS_LEN]; + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + + let balance = unsafe { STATE.get_mut() } + .as_ref() + .map(|state| state.balance_of(&address)) + .unwrap_or(0); + + let addr = sys_alloc([16, 8, 0, 0, 0, 0]); + if addr == 0 { + log!("sys_balance: allocation failed"); + return 0; + } + let bytes = balance.to_le_bytes(); + if !mmu::copy(root_ppn, addr, &bytes) { + logf!("sys_balance: failed to write to 0x%x", addr); + return 0; + } + addr +} diff --git a/crates/kernel/src/syscall/call_program.rs b/crates/kernel/src/syscall/call_program.rs new file mode 100644 index 0000000..4448d78 --- /dev/null +++ b/crates/kernel/src/syscall/call_program.rs @@ -0,0 +1,93 @@ +use clibc::logf; +use types::{Address, ADDRESS_LEN}; + +use crate::global::{CURRENT_TASK, MAX_INPUT_LEN, TASKS}; +use crate::syscall::storage::{caller_address_matches, current_task_root_ppn, read_user_bytes}; +use crate::syscall::SyscallContext; +use crate::task::prep_program_task; +use crate::user_program::with_program_image; + +const REG_COUNT: usize = 32; +const REG_PC: usize = 32; + +pub(crate) fn sys_call_program(args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32 { + let to_ptr = args[0]; + let from_ptr = args[1]; + let input_ptr = args[2]; + let input_len = args[3] as usize; + + if input_len > MAX_INPUT_LEN { + logf!("sys_call_program: input too large"); + return 0; + } + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let to_bytes = match read_user_bytes(root_ppn, to_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let from_bytes = match read_user_bytes(root_ppn, from_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let input = match read_user_bytes(root_ppn, input_ptr, input_len) { + Some(bytes) => bytes, + None => return 0, + }; + + if to_bytes.len() != ADDRESS_LEN || from_bytes.len() != ADDRESS_LEN { + logf!("sys_call_program: invalid address length"); + return 0; + } + + let mut to_buf = [0u8; ADDRESS_LEN]; + let mut from_buf = [0u8; ADDRESS_LEN]; + to_buf.copy_from_slice(&to_bytes); + from_buf.copy_from_slice(&from_bytes); + let to = Address(to_buf); + let from = Address(from_buf); + + if !caller_address_matches(root_ppn, &from) { + logf!("sys_call_program: caller address mismatch"); + return 0; + } + + let task = match with_program_image(&to, |image| { + prep_program_task(&to, &from, image.code, &input, image.entry_off) + }) { + Some(task) => task, + None => return 0, + }; + + let task_idx = unsafe { + let tasks = TASKS.get_mut(); + if tasks.push(task).is_err() { + logf!("sys_call_program: task list full"); + return 0; + } + tasks.len().saturating_sub(1) + }; + + let caller_idx = unsafe { *CURRENT_TASK.get_mut() }; + unsafe { + let tasks = TASKS.get_mut(); + let caller_task = match tasks.get_mut(caller_idx) { + Some(task) => task, + None => { + logf!("sys_call_program: missing caller task %d", caller_idx as u32); + return 0; + } + }; + for (idx, value) in ctx.regs.iter().take(REG_COUNT).enumerate() { + caller_task.tf.regs[idx] = *value; + } + caller_task.tf.pc = ctx.regs[REG_PC].wrapping_add(4); + } + + crate::run_task(task_idx); + 0 +} diff --git a/crates/kernel/src/syscall/fire_event.rs b/crates/kernel/src/syscall/fire_event.rs new file mode 100644 index 0000000..b3f0b0c --- /dev/null +++ b/crates/kernel/src/syscall/fire_event.rs @@ -0,0 +1,34 @@ +use clibc::logf; + +use crate::global::{CURRENT_TX, RECEIPTS}; +use crate::syscall::storage::{current_task_root_ppn, read_user_bytes}; + +pub(crate) fn sys_fire_event(args: [u32; 6]) -> u32 { + let ptr = args[0]; + let len = args[1] as usize; + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let event_bytes = match read_user_bytes(root_ppn, ptr, len) { + Some(bytes) => bytes, + None => return 0, + }; + + let current_idx = unsafe { *CURRENT_TX.get_mut() }; + let receipts = unsafe { RECEIPTS.get_mut() }; + match receipts + .as_mut() + .and_then(|receipts| receipts.get_mut(current_idx)) + { + Some(receipt) => { + receipt.add_event(event_bytes); + } + None => { + logf!("sys_fire_event: missing receipt for tx %d", current_idx as u32); + } + } + 0 +} diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs new file mode 100644 index 0000000..4398c88 --- /dev/null +++ b/crates/kernel/src/syscall/mod.rs @@ -0,0 +1,63 @@ +//! Kernel-owned syscall stubs. These mirror the bootloader syscalls but +//! are now dispatched from the kernel trap handler. Implementations will +//! land here; for now they panic to make missing pieces explicit. +use clibc::{log, logf}; +use clibc::syscalls::{ + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, + SYSCALL_FIRE_EVENT, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, + SYSCALL_TRANSFER, +}; + +pub mod alloc; +pub mod call_program; +pub mod fire_event; +pub mod panic; +pub mod storage; +pub mod balance; + +use alloc::{sys_alloc, sys_dealloc}; +use balance::{sys_balance, sys_transfer}; +use call_program::sys_call_program; +use fire_event::sys_fire_event; +use panic::sys_panic; +use storage::{sys_storage_get, sys_storage_set}; +pub(crate) use panic::sys_panic_with_message; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CallerMode { + User, + Supervisor, +} + +pub struct SyscallContext<'a> { + pub regs: &'a mut [u32], + pub caller_mode: CallerMode, +} + +pub trait SyscallHandler: core::fmt::Debug { + fn handle_syscall(&mut self, call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32; +} + +pub fn dispatch_syscall(call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32 { + match call_id { + SYSCALL_STORAGE_GET => sys_storage_get(args), + SYSCALL_STORAGE_SET => sys_storage_set(args), + SYSCALL_PANIC => sys_panic(args), + SYSCALL_CALL_PROGRAM => sys_call_program(args, ctx), + SYSCALL_FIRE_EVENT => sys_fire_event(args), + SYSCALL_ALLOC => sys_alloc(args), + SYSCALL_DEALLOC => sys_dealloc(args), + SYSCALL_TRANSFER => sys_transfer(args), + SYSCALL_BALANCE => sys_balance(args), + SYSCALL_BRK => sys_brk(args), + _ => { + logf!("unknown syscall id %d", call_id); + 0 + } + } +} + +fn sys_brk(_args: [u32; 6]) -> u32 { + log!("sys_brk: need implementation"); + 0 +} diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs new file mode 100644 index 0000000..c1b1725 --- /dev/null +++ b/crates/kernel/src/syscall/panic.rs @@ -0,0 +1,69 @@ +use clibc::{log, logf}; +use types::{SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; + +use crate::global::{CURRENT_TASK, TASKS}; +use crate::memory::page_allocator as mmu; + +pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { + if msg_ptr == 0 || msg_len == 0 { + log!("sys_panic: empty message"); + halt(); + } + + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get(current) { + Some(task) => task, + None => { + logf!("sys_panic: no current task for slot %d", current as u32); + halt(); + } + }; + let root_ppn = task.addr_space.root_ppn; + + let mut buf = [0u8; 256]; + let mut remaining = core::cmp::min(msg_len as usize, buf.len()); + let mut dst_off = 0usize; + let mut va = msg_ptr; + while remaining > 0 { + let phys = match mmu::translate(root_ppn, va) { + Some(p) => p, + None => { + logf!("sys_panic: invalid msg ptr 0x%x", va); + halt(); + } + }; + let page_off = (va as usize) & (SV32_PAGE_SIZE - 1); + let to_copy = core::cmp::min(remaining, SV32_PAGE_SIZE - page_off); + let src = SV32_DIRECT_MAP_BASE as usize + phys; + unsafe { + core::ptr::copy_nonoverlapping( + src as *const u8, + buf.as_mut_ptr().add(dst_off), + to_copy, + ); + } + remaining -= to_copy; + dst_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + + let msg = &buf[..dst_off]; + if let Ok(s) = core::str::from_utf8(msg) { + logf!("guest panic: %s", s.as_ptr() as u32, s.len() as u32); + } else { + log!("guest panic"); + } + halt(); +} + +pub(crate) fn sys_panic(args: [u32; 6]) -> u32 { + // Legacy path: treat args as [ptr, len] when a0/a1 aren't forwarded. + sys_panic_with_message(args[0], args[1]) +} + +#[inline(never)] +fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs new file mode 100644 index 0000000..0841dd3 --- /dev/null +++ b/crates/kernel/src/syscall/storage.rs @@ -0,0 +1,237 @@ +extern crate alloc; + +use alloc::{format, string::String, vec, vec::Vec}; +use core::cmp; + +use clibc::{log, logf}; +use types::{Address, ADDRESS_LEN, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; + +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; +use crate::memory::page_allocator as mmu; +use crate::syscall::alloc::sys_alloc; +use crate::global::TO_PTR_ADDR; +use state::State; + +pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { + let address_ptr = args[0]; + let domain_ptr = args[1]; + let key_ptr = args[2]; + let lens_packed = args[3] as usize; + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let address_bytes = match read_user_bytes(root_ppn, address_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let mut addr_buf = [0u8; ADDRESS_LEN]; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_storage_get: invalid address length"); + return 0; + } + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + if !caller_address_matches(root_ppn, &address) { + log!("sys_storage_get: address mismatch with caller"); + return 0; + } + + let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { + Some(bytes) => bytes, + None => return 0, + }; + let domain = match core::str::from_utf8(&domain_bytes) { + Ok(s) => s, + Err(_) => { + log!("sys_storage_get: invalid domain utf8"); + return 0; + } + }; + + let key_bytes = match read_user_bytes(root_ppn, key_ptr, key_len) { + Some(bytes) => bytes, + None => return 0, + }; + let key_hex = hex_encode(&key_bytes); + let composite_key = format!("{}:{}", domain, key_hex); + + let value = unsafe { STATE.get_mut() } + .as_ref() + .and_then(|state| state.get_account(&address)) + .and_then(|account| account.storage.get(&composite_key).cloned()); + + let value = match value { + Some(value) => value, + None => return 0, + }; + + let total_len = match value.len().checked_add(4) { + Some(len) => len, + None => { + log!("sys_storage_get: value too large"); + return 0; + } + }; + if total_len > u32::MAX as usize { + log!("sys_storage_get: value exceeds u32 size"); + return 0; + } + + let addr = sys_alloc([total_len as u32, 8, 0, 0, 0, 0]); + if addr == 0 { + log!("sys_storage_get: allocation failed"); + return 0; + } + + let mut buf = Vec::with_capacity(total_len); + buf.extend_from_slice(&(value.len() as u32).to_le_bytes()); + buf.extend_from_slice(&value); + + if !mmu::copy(root_ppn, addr, &buf) { + logf!("sys_storage_get: failed to write to 0x%x", addr); + return 0; + } + + addr +} + +pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { + let address_ptr = args[0]; + let domain_ptr = args[1]; + let key_ptr = args[2]; + let lens_packed = args[3] as usize; + let val_ptr = args[4]; + let val_len = args[5] as usize; + + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; + + let root_ppn = match current_task_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let address_bytes = match read_user_bytes(root_ppn, address_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_storage_set: invalid address length"); + return 0; + } + let mut addr_buf = [0u8; ADDRESS_LEN]; + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + if !caller_address_matches(root_ppn, &address) { + log!("sys_storage_set: address mismatch with caller"); + return 0; + } + + let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { + Some(bytes) => bytes, + None => return 0, + }; + let domain = match core::str::from_utf8(&domain_bytes) { + Ok(s) => s, + Err(_) => { + log!("sys_storage_set: invalid domain utf8"); + return 0; + } + }; + + let key_bytes = match read_user_bytes(root_ppn, key_ptr, key_len) { + Some(bytes) => bytes, + None => return 0, + }; + let key_hex = hex_encode(&key_bytes); + + let value = match read_user_bytes(root_ppn, val_ptr, val_len) { + Some(bytes) => bytes, + None => return 0, + }; + + let composite_key = format!("{}:{}", domain, key_hex); + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + state + .get_account_mut(&address) + .storage + .insert(composite_key, value); + 0 +} + +pub(crate) fn current_task_root_ppn() -> Option { + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + match tasks.get(current) { + Some(task) => Some(task.addr_space.root_ppn), + None => { + logf!("sys_storage: no current task for slot %d", current as u32); + None + } + } +} + +pub(crate) fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option> { + if len == 0 { + return Some(Vec::new()); + } + let mut buf = vec![0u8; len]; + let mut remaining = len; + let mut dst_off = 0usize; + let mut va = ptr; + while remaining > 0 { + let phys = match mmu::translate(root_ppn, va) { + Some(p) => p, + None => { + logf!("sys_storage: invalid memory access 0x%x", va); + return None; + } + }; + let page_off = (va as usize) & (SV32_PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, SV32_PAGE_SIZE - page_off); + let src = SV32_DIRECT_MAP_BASE as usize + phys; + unsafe { + core::ptr::copy_nonoverlapping( + src as *const u8, + buf.as_mut_ptr().add(dst_off), + to_copy, + ); + } + remaining -= to_copy; + dst_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + Some(buf) +} + +pub(crate) fn caller_address_matches(root_ppn: u32, address: &Address) -> bool { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + return true; + } + let caller_bytes = match read_user_bytes(root_ppn, TO_PTR_ADDR, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return false, + }; + if caller_bytes.len() != ADDRESS_LEN { + return false; + } + let mut caller_buf = [0u8; ADDRESS_LEN]; + caller_buf.copy_from_slice(&caller_bytes); + Address(caller_buf) == *address +} + +fn hex_encode(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = Vec::with_capacity(bytes.len().saturating_mul(2)); + for &b in bytes { + out.push(HEX[(b >> 4) as usize]); + out.push(HEX[(b & 0x0f) as usize]); + } + String::from_utf8(out).unwrap_or_default() +} diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs new file mode 100644 index 0000000..906ea1a --- /dev/null +++ b/crates/kernel/src/task/mod.rs @@ -0,0 +1,99 @@ +#![allow(dead_code)] + +// Program launch flow (kernel side) +// --------------------------------- +// Goals: +// - Create a fresh address space for each program call (new root PPN + ASID). +// - Map a fixed, contiguous user window starting at VA 0x0 that holds: +// * Code/rodata (program bytes copied starting at VA 0x0; entry at `entry_off`) +// * A user stack (STACK_BYTES) +// * A user heap (HEAP_BYTES) with call args in a dedicated page at INPUT_BASE_ADDR +// - Copy call arguments (to/from addresses + input buffer) into that call-args page. +// - Prepare a trapframe with PC/SP/args and transfer control to user code. +// +// Key pieces: +// - PROGRAM_WINDOW_BYTES covers code + rodata + stack + heap: a single map call per program. +// - TRAMPOLINE_VA is one page immediately after the user window, mapped into both +// the kernel root and the new user root. It contains: +// * an entry trampoline that switches satp and sret's into user mode +// * a trap trampoline that switches satp back to the kernel root and jumps +// to the real trap_entry +// This keeps trap entry valid even when the current root is the user page table. +// +// prep_program_task(to, from, code, input, entry_off): +// 1) Allocate ASID and a fresh root PPN; map the user window + call-args page. +// 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). +// 3) Map the trampoline page into the user root and mirror the same physical page +// into the current kernel root; write trampoline code into it. +// 4) Build a Task with AddressSpace {root_ppn, asid} and set trapframe: +// pc = PROGRAM_VA_BASE + entry_off +// sp = top of user stack within the window +// a0..a3 = to/from/input_base/input_len +// Caller can push the task into TASKS for bookkeeping. +// +// kernel_run_task(task): +// - Save the current kernel register file (x0-x31 + pc) into TASKS[0]. +// - Run the task (same behavior as run_task). +// +// run_task(task): +// - Preload t0 with the task root (satp value); load user sp and a0..a3; clear ra. +// - Set sepc to the user PC and clear sstatus.SPP so sret enters user mode. +// - Set stvec to the trap trampoline VA. +// - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp +// to the new root, and executes sret into user code. There is no return +// path yet; this is a one-way handoff. +// +// Notes: +// - The window and trampoline VAs are low for simplicity; nothing here relocates. +// - We currently do not touch sstatus/mstatus or perform sfence.vma; add those +// when modeling fuller privilege transitions. + +use crate::global::NEXT_ASID; + +pub mod task; +pub mod prep; +pub mod run; +mod trampoline; + +pub use task::{AddressSpace, Task, TrapFrame}; +pub use prep::prep_program_task; +pub use run::{kernel_run_task, run_task}; + +const PAGE_SIZE: usize = 4096; +const STACK_BYTES: usize = crate::global::STACK_BYTES; +pub const HEAP_BYTES: usize = crate::global::HEAP_BYTES; +// Location of the page that hosts the satp-switch trampolines. Kept just past +// the user window so it does not collide with program text/stack/heap. This VA +// is mapped into both roots so satp can be switched without invalidating the +// instruction stream mid-flight. +pub const TRAMPOLINE_VA: u32 = + (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES + 0x10000) as u32; // Shared page outside the kernel window. +const TRAP_TRAMPOLINE_OFFSET: usize = 0x10; // Offset for the trap-entry stub within the page. +pub const TRAP_TRAMPOLINE_VA: u32 = + TRAMPOLINE_VA + TRAP_TRAMPOLINE_OFFSET as u32; // stvec target for user-mode traps. +pub use crate::global::{PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; + +const REG_SP: usize = 2; +const REG_RA: usize = 1; +const REG_A0: usize = 10; +const REG_A1: usize = 11; +const REG_A2: usize = 12; +const REG_A3: usize = 13; +// Raw RISC-V words for the entry trampoline used to switch satp safely while +// executing from a page mapped in both the kernel and user roots. The kernel +// loads t0 = target satp before entering this stub so we can change roots +// and return to user mode at sepc without returning to unmapped kernel text. +// t0: target satp value. +const TRAMPOLINE_CODE: [u32; 2] = [ + 0x1802_9073, // csrw satp, t0 + 0x1020_0073, // sret +]; + +pub(super) fn alloc_asid() -> u16 { + unsafe { + let counter = NEXT_ASID.get_mut(); + let asid = if *counter == 0 { 1 } else { *counter }; + *counter = asid.wrapping_add(1); + asid + } +} diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs new file mode 100644 index 0000000..e8906cd --- /dev/null +++ b/crates/kernel/src/task/prep.rs @@ -0,0 +1,193 @@ +use crate::{AddressSpace, Task}; +use crate::global::{ + CALL_ARGS_PAGE_BASE, CODE_SIZE_LIMIT, CURRENT_TASK, FROM_PTR_ADDR, HEAP_START_ADDR, + INPUT_BASE_ADDR, MAX_INPUT_LEN, RO_DATA_SIZE_LIMIT, TO_PTR_ADDR, +}; +use crate::memory::page_allocator as mmu; +use clibc::{log, logf}; +use types::address::Address; +use types::SV32_PAGE_SIZE; + +use super::{ + alloc_asid, trampoline::map_trampoline_page, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, REG_A0, + REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, +}; + +/// Create a new task for a program and map its virtual address window via syscalls. +/// +/// This sets up: +/// - Maps a fixed VA window [PROGRAM_VA_BASE, PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES). +/// - Returns a Task with the new address space. +/// +/// The caller is responsible for copying program bytes into the mapped window +/// and initializing the user trapframe (PC/SP/args) before running. +pub fn prep_program_task( + to: &Address, + from: &Address, + code: &[u8], + input: &[u8], + entry_off: u32, +) -> Option { + if input.len() > MAX_INPUT_LEN { + log!("launch_program: input too large"); + return None; + } + + let asid = alloc_asid(); + let root_ppn = match mmu::alloc_root() { + Some(ppn) => ppn, + None => { + logf!("launch_program: no free root PPN available"); + return None; + } + }; + + let window_end = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); + logf!( + "launch_program: asid=%d root=0x%x map=[0x%x,0x%x)", + asid as u32, + root_ppn, + PROGRAM_VA_BASE, + window_end + ); + let args_perms = mmu::PagePerms::new(true, false, false, true); + if !mmu::map_range_for_root(root_ppn, CALL_ARGS_PAGE_BASE, SV32_PAGE_SIZE, args_perms) { + panic!( + "launch_program: failed to map call-args page (root=0x{:x})", + root_ppn + ); + } + map_program_window(root_ppn, code.len()); + + // Copy the full program image starting at VA 0 so section offsets (e.g. .text at 0x400) + // land where the ELF expected them. Entry offset is provided by the caller. + if entry_off as usize >= code.len() { + panic!("launch_program: invalid entry offset"); + } + if code.len() >= entry_off as usize + 8 { + let head = u32::from_le_bytes([ + code[entry_off as usize], + code[entry_off as usize + 1], + code[entry_off as usize + 2], + code[entry_off as usize + 3], + ]); + let head2 = u32::from_le_bytes([ + code[entry_off as usize + 4], + code[entry_off as usize + 5], + code[entry_off as usize + 6], + code[entry_off as usize + 7], + ]); + } + let nz_count = code.iter().filter(|&&b| b != 0).count(); + let local_first_nz = code.iter().position(|&b| b != 0).unwrap_or(code.len()); + + if !mmu::copy(root_ppn, PROGRAM_VA_BASE, code) { + logf!("launch_program: failed to copy code into root=0x%x", root_ppn); + return None; + } + + if !mmu::copy(root_ppn, TO_PTR_ADDR, &to.0) { + logf!("launch_program: failed to copy 'to' address into root=0x%x", root_ppn); + return None; + } + if !mmu::copy(root_ppn, FROM_PTR_ADDR, &from.0) { + logf!("launch_program: failed to copy 'from' address into root=0x%x", root_ppn); + return None; + } + if !mmu::copy(root_ppn, INPUT_BASE_ADDR, input) { + panic!( + "prep_program_task: failed to copy input into root=0x{:x}", + root_ppn + ); + } + + // Sanity check where the code landed in the user root. + let entry_va = PROGRAM_VA_BASE.wrapping_add(entry_off); + let user_phys = mmu::translate(root_ppn, entry_va).unwrap_or(usize::MAX); + let user_word = mmu::peek_word(root_ppn, entry_va).unwrap_or(0); + logf!( + "prep_program_task: code VA=0x%x user_phys=0x%x user_word=0x%x code_start=0x%x", + entry_va, + user_phys as u32, + user_word, + entry_off + ); + map_trampoline_page(root_ppn); + + let mut task = Task::new( + AddressSpace::new( + root_ppn, + asid, + PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES as u32, + ), + HEAP_START_ADDR as u32, + ); + let caller = unsafe { *CURRENT_TASK.get_mut() }; + task.caller_task_id = Some(caller); + // Set up initial trapframe. + let stack_top = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); + task.tf.pc = entry_va; + task.tf.regs[REG_SP] = stack_top; + task.tf.regs[REG_A0] = TO_PTR_ADDR; + task.tf.regs[REG_A1] = FROM_PTR_ADDR; + task.tf.regs[REG_A2] = INPUT_BASE_ADDR; + task.tf.regs[REG_A3] = input.len() as u32; + logf!( + "prep_program_task: trapframe pc=0x%x sp=0x%x a0=0x%x a1=0x%x a2=0x%x a3=%d", + task.tf.pc, + task.tf.regs[REG_SP], + task.tf.regs[REG_A0], + task.tf.regs[REG_A1], + task.tf.regs[REG_A2], + task.tf.regs[REG_A3], + ); + // Also log the expected user stack window for sanity. + let stack_base = stack_top.saturating_sub(STACK_BYTES as u32); + logf!( + "prep_program_task: stack window=[0x%x,0x%x) heap_base=0x%x", + stack_base, + stack_top, + HEAP_START_ADDR as u32 + ); + + Some(task) +} + +fn align_up(value: usize, align: usize) -> usize { + if align == 0 { + return value; + } + (value + (align - 1)) & !(align - 1) +} + +/// Map the program window so code pages are RX and data/stack/heap are RW. +/// The first page stays RWX because the program writes its result at 0x100. +fn map_program_window(root_ppn: u32, code_len: usize) { + let code_len = align_up(code_len, SV32_PAGE_SIZE); + if code_len > PROGRAM_WINDOW_BYTES { + panic!("launch_program: code window exceeds program window"); + } + let first_page_len = core::cmp::min(code_len, SV32_PAGE_SIZE); + let first_page_perms = mmu::PagePerms::user_rwx(); + // Page 0 hosts the result header at 0x100, so keep it writable. + if !mmu::map_range_for_root(root_ppn, PROGRAM_VA_BASE, first_page_len, first_page_perms) { + panic!("launch_program: first page mapping failed (root=0x{:x})", root_ppn); + } + if code_len > SV32_PAGE_SIZE { + let code_perms = mmu::PagePerms::new(true, false, true, true); + let code_start = PROGRAM_VA_BASE.wrapping_add(SV32_PAGE_SIZE as u32); + let code_rest = code_len.saturating_sub(SV32_PAGE_SIZE); + // Remaining code pages are RX-only to protect program text. + if !mmu::map_range_for_root(root_ppn, code_start, code_rest, code_perms) { + panic!("launch_program: code mapping failed (root=0x{:x})", root_ppn); + } + } + let data_start = PROGRAM_VA_BASE.wrapping_add(code_len as u32); + let data_len = PROGRAM_WINDOW_BYTES.saturating_sub(code_len); + let data_perms = mmu::PagePerms::new(true, true, false, true); + // Data/stack/heap region is RW, non-exec. + if !mmu::map_range_for_root(root_ppn, data_start, data_len, data_perms) { + panic!("launch_program: data mapping failed (root=0x{:x})", root_ppn); + } +} diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs new file mode 100644 index 0000000..b6674db --- /dev/null +++ b/crates/kernel/src/task/run.rs @@ -0,0 +1,161 @@ +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::memory::page_allocator as mmu; +use clibc::logf; + +use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; + +const SSTATUS_SPP: u32 = 1 << 8; +const REG_COUNT: usize = 32; +const REG_PC: usize = 32; +const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc +const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; +const REG_RA: usize = 1; + +/// One-way context switch into a user task: +/// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) +pub fn run_task(task_idx: usize) { + let (target_root, asid, pc, sp, a0, a1, a2, a3) = unsafe { + let tasks = TASKS.get_mut(); + let task = match tasks.get(task_idx) { + Some(task) => task, + None => { + logf!("run_task: invalid task slot %d", task_idx as u32); + return; + } + }; + ( + task.addr_space.root_ppn, + task.addr_space.asid, + task.tf.pc, + task.tf.regs[REG_SP], + task.tf.regs[REG_A0], + task.tf.regs[REG_A1], + task.tf.regs[REG_A2], + task.tf.regs[REG_A3], + ) + }; + unsafe { + *CURRENT_TASK.get_mut() = task_idx; + } + let kernel_root = mmu::current_root(); + logf!( + "run_task: switching satp 0x%x -> 0x%x asid=%d pc=0x%x sp=0x%x", + kernel_root, + target_root, + asid as u32, + pc, + sp, + ); + unsafe { + if let Some(task) = TASKS.get_mut().get(task_idx) { + if let Some(caller_idx) = task.caller_task_id { + if let Some(caller_task) = TASKS.get_mut().get(caller_idx) { + logf!( + "run_task: return ra=0x%x sp=0x%x for caller %d", + caller_task.tf.regs[REG_RA], + caller_task.tf.regs[REG_SP], + caller_idx as u32 + ); + } + } + } + } + // Prepare to enter user mode via sret: set sepc and clear sstatus.SPP. + let mut sstatus: u32; + unsafe { + core::arch::asm!("csrr {0}, sstatus", out(reg) sstatus); + } + sstatus &= !SSTATUS_SPP; + unsafe { + core::arch::asm!("csrw sstatus, {0}", in(reg) sstatus); + core::arch::asm!("csrw sepc, {0}", in(reg) pc); + core::arch::asm!("csrw stvec, {0}", in(reg) TRAP_TRAMPOLINE_VA); + } + // Update the helper's view of the current root before switching. + mmu::set_current_root(target_root); + // Set up registers and jump to the shared trampoline page (mapped in both + // the kernel and user roots). The trampoline will write satp and transfer + // control to the user PC. + unsafe { + core::arch::asm!( + "mv ra, zero # clear return address for one-way jump", + "mv sp, t2 # load user stack pointer", + "jr t3 # jump to shared trampoline", + in("t0") target_root, + in("a0") a0, + in("a1") a1, + in("a2") a2, + in("a3") a3, + in("t2") sp, + in("t3") TRAMPOLINE_VA, + options(noreturn) + ); + } +} + +/// Save the full kernel register set into TASKS[0] and then run the task. +#[unsafe(naked)] +pub unsafe extern "C" fn kernel_run_task(task_idx: usize) -> ! { + core::arch::naked_asm!( + "addi sp, sp, -{frame_bytes} # reserve space for regs + pc", + "sw zero, 0(sp) # save x0", + "sw ra, 4(sp) # save x1", + "sw t1, 24(sp) # save x6 before clobber", + "addi t1, sp, {frame_bytes} # compute original sp", + "sw t1, 8(sp) # save x2 (original sp)", + "sw gp, 12(sp) # save x3", + "sw tp, 16(sp) # save x4", + "sw t0, 20(sp) # save x5", + "sw t2, 28(sp) # save x7", + "sw s0, 32(sp) # save x8", + "sw s1, 36(sp) # save x9", + "sw a0, 40(sp) # save x10", + "sw a1, 44(sp) # save x11", + "sw a2, 48(sp) # save x12", + "sw a3, 52(sp) # save x13", + "sw a4, 56(sp) # save x14", + "sw a5, 60(sp) # save x15", + "sw a6, 64(sp) # save x16", + "sw a7, 68(sp) # save x17", + "sw s2, 72(sp) # save x18", + "sw s3, 76(sp) # save x19", + "sw s4, 80(sp) # save x20", + "sw s5, 84(sp) # save x21", + "sw s6, 88(sp) # save x22", + "sw s7, 92(sp) # save x23", + "sw s8, 96(sp) # save x24", + "sw s9, 100(sp) # save x25", + "sw s10, 104(sp) # save x26", + "sw s11, 108(sp) # save x27", + "sw t3, 112(sp) # save x28", + "sw t4, 116(sp) # save x29", + "sw t5, 120(sp) # save x30", + "sw t6, 124(sp) # save x31", + "auipc t1, 0 # read current pc", + "sw t1, 128(sp) # save pc", + "mv a1, a0 # move task_idx into a1", + "mv a0, sp # pass saved regs pointer in a0", + "call {helper} # save into kernel task and run", + frame_bytes = const TRAP_FRAME_BYTES, + helper = sym kernel_run_task_inner, + ); +} + +/// Save the kernel trapframe into TASKS[0] and then jump into the requested task. +extern "C" fn kernel_run_task_inner(saved: *const u32, task_idx: usize) -> ! { + // Interpret the saved trap-frame as regs[0..31] + pc and copy it into TASKS[0]. + let regs = unsafe { core::slice::from_raw_parts(saved, TRAP_FRAME_WORDS) }; + let kernel_root = mmu::current_root(); + unsafe { + let tasks = TASKS.get_mut(); + if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { + kernel_task.addr_space.root_ppn = kernel_root; + for (idx, value) in regs.iter().take(REG_COUNT).enumerate() { + kernel_task.tf.regs[idx] = *value; + } + kernel_task.tf.pc = regs[REG_PC]; + } + } + run_task(task_idx); + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/crates/kernel/src/task/task.rs b/crates/kernel/src/task/task.rs new file mode 100644 index 0000000..c89f5f2 --- /dev/null +++ b/crates/kernel/src/task/task.rs @@ -0,0 +1,79 @@ +use core::fmt; +use types::result::Result as VmResult; + +/// Minimal trapframe capturing user-visible registers on trap/return. +/// This mirrors RISC-V general-purpose regs plus PC. +#[derive(Clone, Copy, Default)] +pub struct TrapFrame { + /// General-purpose registers x0-x31 (x0 is always zero when restored). + pub regs: [u32; 32], + /// Program counter to resume at when returning to user. + pub pc: u32, +} + +impl fmt::Debug for TrapFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrapFrame") + .field("pc", &format_args!("0x{:08x}", self.pc)) + .finish() + } +} + +/// Describes a process/thread address space. +/// In a real kernel this would own the page table root PPN and ASID. +#[derive(Debug, Clone, Copy)] +pub struct AddressSpace { + /// Root page-table PPN (satp PPN field) for this address space. + pub root_ppn: u32, + /// Optional address-space identifier (ASID); zero if unused. + pub asid: u16, + /// Base virtual address for this address space's mapped window. + pub va_base: u32, + /// Size in bytes of the mapped virtual window. + pub va_len: u32, +} + +impl AddressSpace { + pub fn new(root_ppn: u32, asid: u16, va_base: u32, va_len: u32) -> Self { + Self { + root_ppn, + asid, + va_base, + va_len, + } + } +} + +/// Kernel-owned per-task state. This is where the kernel stores the +/// current address-space root and the saved trapframe. +#[derive(Debug)] +pub struct Task { + /// Saved user trapframe (regs + pc) to restore on return. + pub tf: TrapFrame, + /// Address space for this task (page-table root/asid). + pub addr_space: AddressSpace, + /// Next heap pointer for this task (virtual address). + pub heap_ptr: u32, + /// Task slot that initiated this task, if any. + pub caller_task_id: Option, + /// Last decoded program result for this task, if any. + pub last_result: Option, +} + +impl Task { + pub fn new(addr_space: AddressSpace, heap_ptr: u32) -> Self { + Self { + tf: TrapFrame::default(), + addr_space, + heap_ptr, + caller_task_id: None, + last_result: None, + } + } + + /// Create the initial kernel task. This represents the supervisor itself: + /// - `root_ppn` is the kernel page-table root PPN that will be loaded into satp. + pub fn kernel(root_ppn: u32, heap_ptr: u32, va_base: u32, va_len: u32) -> Self { + Task::new(AddressSpace::new(root_ppn, 0, va_base, va_len), heap_ptr) + } +} diff --git a/crates/kernel/src/task/trampoline.rs b/crates/kernel/src/task/trampoline.rs new file mode 100644 index 0000000..1a91b24 --- /dev/null +++ b/crates/kernel/src/task/trampoline.rs @@ -0,0 +1,107 @@ +use crate::global::{KERNEL_TASK_SLOT, TASKS}; +use crate::memory::page_allocator as mmu; + +use super::{ + PAGE_SIZE, TRAMPOLINE_CODE, TRAMPOLINE_VA, TRAP_TRAMPOLINE_OFFSET, +}; + +const REG_T0: u32 = 5; +const REG_T1: u32 = 6; +const REG_T2: u32 = 7; +const TRAP_TRAMPOLINE_WORDS: usize = 7; // csrr + 2x(hi/lo) + csrw + jalr + +fn split_imm(val: u32) -> (u32, i32) { + // Build a LUI/ADDI pair for a full 32-bit immediate. + let hi = ((val as u64 + 0x800) >> 12) as u32; + let lo = val as i64 - ((hi as i64) << 12); + (hi, lo as i32) +} + +fn encode_lui(rd: u32, imm20: u32) -> u32 { + (imm20 << 12) | (rd << 7) | 0x37 +} + +fn encode_addi(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x13 +} + +fn encode_jalr(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x67 +} + +fn encode_csrr(rd: u32, csr: u32) -> u32 { + (csr << 20) | (0 << 15) | (0b010 << 12) | (rd << 7) | 0x73 +} + +/// Build the trap-entry trampoline instructions. +/// +/// This stub runs at `TRAP_TRAMPOLINE_VA` while still in the user address space. +/// It saves the current user `satp` into `t0`, switches to the kernel root page +/// table, and jumps to the real kernel trap handler at `trap_entry`. +fn build_trap_trampoline(kernel_satp: u32, trap_entry: u32) -> [u32; TRAP_TRAMPOLINE_WORDS] { + // Assemble the kernel satp and trap_entry as LUI/ADDI pairs. + let (satp_hi, satp_lo) = split_imm(kernel_satp); + let (entry_hi, entry_lo) = split_imm(trap_entry); + [ + encode_csrr(REG_T0, 0x180), // csrr t0, satp: save user satp so kernel can restore later. + encode_lui(REG_T1, satp_hi), // lui t1, %hi(kernel_satp): load upper bits. + encode_addi(REG_T1, REG_T1, satp_lo), // addi t1, t1, %lo(kernel_satp): finish satp. + 0x1803_1073, // csrw satp, t1: switch to kernel page table. + encode_lui(REG_T2, entry_hi), // lui t2, %hi(trap_entry): load trap handler addr. + encode_addi(REG_T2, REG_T2, entry_lo), // addi t2, t2, %lo(trap_entry). + encode_jalr(0, REG_T2, 0), // jalr x0, t2, 0: jump to trap handler. + ] +} + +pub(super) fn map_trampoline_page(root_ppn: u32) { + // Install a small trampoline page mapped in both roots so we can switch + // satp safely before jumping into the user program. + let kernel_tramp_perms = mmu::PagePerms::kernel_rwx(); + let user_tramp_perms = mmu::PagePerms::new(true, false, true, true); + let kernel_root = unsafe { + TASKS + .get_mut() + .get(KERNEL_TASK_SLOT) + .map(|task| task.addr_space.root_ppn) + .unwrap_or_else(mmu::current_root) + }; + let trap_entry = crate::trap::trap_entry as usize as u32; + let trap_trampoline = build_trap_trampoline(kernel_root, trap_entry); + // Stash both trampolines in a single shared page. + let mut tramp_bytes = + [0u8; TRAP_TRAMPOLINE_OFFSET + TRAP_TRAMPOLINE_WORDS * 4]; + for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { + tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); + } + for (i, word) in trap_trampoline.iter().enumerate() { + // Trap stub lives at TRAP_TRAMPOLINE_OFFSET for stvec to target. + let base = TRAP_TRAMPOLINE_OFFSET + i * 4; + tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); + } + if !mmu::map_range_for_root( + kernel_root, + TRAMPOLINE_VA, + PAGE_SIZE, + kernel_tramp_perms, + ) { + panic!("prep_program_task: failed to map trampoline page in kernel root"); + } + if !mmu::copy(kernel_root, TRAMPOLINE_VA, &tramp_bytes) { + panic!("prep_program_task: failed to populate trampoline code"); + } + let tramp_phys = match mmu::translate(kernel_root, TRAMPOLINE_VA) { + Some(p) => p as u32, + None => { + panic!("prep_program_task: trampoline VA not mapped in kernel root"); + } + }; + if !mmu::map_physical_range_for_root( + root_ppn, + TRAMPOLINE_VA, + tramp_phys, + PAGE_SIZE, + user_tramp_perms, + ) { + panic!("prep_program_task: failed to map trampoline page in user root"); + } +} diff --git a/crates/kernel/src/tests/results.rs b/crates/kernel/src/tests/results.rs new file mode 100644 index 0000000..c013666 --- /dev/null +++ b/crates/kernel/src/tests/results.rs @@ -0,0 +1,29 @@ +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TestResults { + pub status: u32, + pub detail: u32, +} + +impl TestResults { + pub const fn pass(detail: u32) -> Self { + Self { + status: 0, + detail, + } + } + + pub const fn fail(detail: u32) -> Self { + Self { + status: 1, + detail, + } + } +} + +pub unsafe fn write_results(results: TestResults) { + let ptr = kernel::global::KERNEL_RESULT_ADDR as *mut TestResults; + unsafe { + ptr.write_volatile(results); + } +} diff --git a/crates/kernel/src/tests/utils.rs b/crates/kernel/src/tests/utils.rs new file mode 100644 index 0000000..cd8534b --- /dev/null +++ b/crates/kernel/src/tests/utils.rs @@ -0,0 +1,39 @@ +use clibc::log; +use kernel::memory::{heap, page_allocator}; +use kernel::{trap, BootInfo}; +use crate::results; + +#[path = "../init.rs"] +mod init; + +pub fn init_test_kernel(boot_info_ptr: *const BootInfo) -> BootInfo { + let boot_info = unsafe { boot_info_ptr.as_ref() }; + if let Some(info) = init::init_boot_info(boot_info) { + unsafe { + page_allocator::init(info); + heap::init(info.heap_ptr, info.va_base, info.va_len); + } + trap::init_trap_vector(info.kstack_top); + let info_copy = *info; + log!("kernel initialized"); + info_copy + } else { + panic!("init_test_kernel: missing boot info"); + } +} + +pub fn pass() -> ! { + unsafe { results::write_results(results::TestResults::pass(0)) }; + halt(); +} + +pub fn fail(code: u32) -> ! { + unsafe { results::write_results(results::TestResults::fail(code)) }; + halt(); +} + +#[inline(never)] +pub fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs new file mode 100644 index 0000000..ffa5b0c --- /dev/null +++ b/crates/kernel/src/trap/mod.rs @@ -0,0 +1,330 @@ +use core::arch::asm; +use clibc::{log, logf}; +use types::result::{Result as VmResult, RESULT_DATA_SIZE}; + +use crate::global::{ + CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, MAX_RESULT_SIZE, RESULT_ADDR, TASKS, +}; +use crate::memory::page_allocator as mmu; +use crate::syscall; +use crate::syscall::alloc::alloc_in_task; +use crate::syscall::storage::read_user_bytes; +use crate::task::TRAMPOLINE_VA; +use crate::Task; + +mod save_trap_frame; +mod restore_trap_frame; + +use restore_trap_frame::restore_trap_frame; +use save_trap_frame::save_trap_frame; + +const SCAUSE_ECALL_FROM_U: usize = 8; +const SCAUSE_ECALL_FROM_S: usize = 9; +const SCAUSE_BREAKPOINT: usize = 3; +const SSTATUS_SPP: u32 = 1 << 8; +const REG_COUNT: usize = 32; +const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc +const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; +const REG_RA: usize = 1; +const REG_A0: usize = 10; +const REG_A1: usize = 11; +const REG_A2: usize = 12; +const REG_A3: usize = 13; +const REG_A4: usize = 14; +const REG_A5: usize = 15; +const REG_A6: usize = 16; +const REG_A7: usize = 17; +const REG_SP: usize = 2; +const REG_PC: usize = 32; + +/// Install the kernel trap vector and set up the kernel stack for traps. +pub fn init_trap_vector(kstack_top: u32) { + // Seed sscratch with the kernel stack top so trap entry can swap sp with + // sscratch and immediately land on a known-good kernel stack. + logf!("init_trap_vector: kstack_top=0x%x", kstack_top); + unsafe { + asm!("csrw sscratch, {0}", in(reg) kstack_top); + asm!("csrw stvec, {0}", in(reg) trap_entry as usize); + } +} + +/// Trap entry stub: +/// - Switch to the kernel stack via sscratch. +/// - Save sepc, ra, a0-a7, and t0 (user satp). +/// - Call into the Rust trap handler with a pointer to the saved area. +/// - Restore registers and return via the shared trampoline. +// #[unsafe(naked)] +pub unsafe extern "C" fn trap_entry() -> ! { + unsafe { + core::arch::asm!( + "call {swap} # switch to kernel stack and reserve trap frame", + "call {save} # save regs on kernel stack", + "mv s0, a0 # preserve trap frame pointer across handle_trap", + "call {ensure} # restore kernel root if we trapped from user", + "mv a0, s0 # restore trap frame pointer for handle_trap", + "call {handler} # run Rust trap handler", + "mv a2, a1 # stash return kind", + "mv a1, s0 # restore trap frame pointer for restore", + "j {restore}", + swap = sym swap_to_kernel_stack, + save = sym save_trap_frame, + ensure = sym ensure_kernel_root_for_trap, + handler = sym handle_trap, + restore = sym restore_trap_frame, + options(noreturn), + ); + } +} + +#[unsafe(naked)] +unsafe extern "C" fn swap_to_kernel_stack() -> ! { + core::arch::naked_asm!( + // Swap sp with sscratch: + // - On trap entry, sscratch holds the kernel stack top. + // - After the swap, sp points at the kernel stack and the previous sp + // (user sp) is saved in sscratch for later restoration. + "csrrw sp, sscratch, sp", + // Reserve space for the trap frame on the kernel stack. + "addi sp, sp, -{frame_bytes}", + "ret", + frame_bytes = const TRAP_FRAME_BYTES, + ); +} + +#[unsafe(naked)] +unsafe extern "C" fn return_from_trap() -> ! { + core::arch::naked_asm!( + "csrr t1, sstatus", + "andi t1, t1, {spp}", + "bnez t1, 1f", + "li t1, {tramp}", + "jr t1", + "1:", + "sret", + tramp = const TRAMPOLINE_VA, + spp = const SSTATUS_SPP, + ); +} + +/// Rust-level trap handler. Receives a pointer to the saved register block +/// laid out as: +/// regs[0..32] = x0..x31, regs[32] = pc. +#[unsafe(no_mangle)] +pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { + let regs = unsafe { core::slice::from_raw_parts_mut(saved, TRAP_FRAME_WORDS) }; + let scause = read_scause(); + let stval = read_stval(); + let sepc = regs[REG_PC]; + + let is_interrupt = (scause >> 31) != 0; + if is_interrupt { + panic!( + "unexpected interrupt trap: scause=0x{:x} stval=0x{:x} sepc=0x{:08x}", + scause, stval, sepc + ); + } + + let code = scause & 0xfff; + let mut return_kind = if read_sstatus() & SSTATUS_SPP != 0 { 1 } else { 0 }; + let mut return_sp = regs[REG_SP]; + match code { + SCAUSE_ECALL_FROM_U | SCAUSE_ECALL_FROM_S => { + let args = [ + regs[REG_A1], + regs[REG_A2], + regs[REG_A3], + regs[REG_A4], + regs[REG_A5], + regs[REG_A6], + ]; + let call_id = regs[REG_A7]; + let caller_mode = if read_sstatus() & SSTATUS_SPP != 0 { + syscall::CallerMode::Supervisor + } else { + syscall::CallerMode::User + }; + let ret = { + let mut ctx = syscall::SyscallContext { regs, caller_mode }; + syscall::dispatch_syscall(call_id, args, &mut ctx) + }; + regs[REG_A0] = ret; // a0 return value + regs[REG_PC] = regs[REG_PC].wrapping_add(4); // Advance past ecall + return_kind = 0; + return_sp = regs[REG_SP]; + } + SCAUSE_BREAKPOINT => { + // Default to returning to the kernel task unless the current task has a caller. + let mut caller_idx = KERNEL_TASK_SLOT; + let mut result_for_caller: Option = None; + unsafe { + let current = *CURRENT_TASK.get_mut(); + let tasks = TASKS.get_mut(); + // If this is a user task, save its current trapframe so it can be resumed later. + if current != KERNEL_TASK_SLOT { + if let Some(task) = tasks.get_mut(current) { + if let Some(result) = read_task_result(task) { + task.last_result = Some(result); + log_task_result(&result); + result_for_caller = Some(result); + } else { + log!("program result: failed to read result bytes"); + } + for (idx, value) in regs.iter().take(REG_COUNT).enumerate() { + task.tf.regs[idx] = *value; + } + task.tf.pc = regs[REG_PC]; + // Use the recorded caller task as the return target. + caller_idx = task.caller_task_id.unwrap_or(KERNEL_TASK_SLOT); + if caller_idx == KERNEL_TASK_SLOT { + // Only record tasks that return to the kernel so bundle resume can + // associate the completed task with the current transaction receipt. + *LAST_COMPLETED_TASK.get_mut() = Some(current); + } + } + } + // Restore the caller task's trapframe and address-space root. + if let Some(caller_task) = tasks.get_mut(caller_idx) { + if caller_idx != KERNEL_TASK_SLOT { + let result_ptr = match result_for_caller { + Some(result) => write_result_to_caller(caller_task, &result).unwrap_or(0), + None => 0, + }; + caller_task.tf.regs[REG_A0] = result_ptr; + } + for (idx, value) in caller_task.tf.regs.iter().take(REG_COUNT).enumerate() { + regs[idx] = *value; + } + // Resume at the caller's return address. + regs[REG_PC] = if caller_idx == KERNEL_TASK_SLOT { + caller_task.tf.regs[REG_RA] + } else { + caller_task.tf.pc + }; + mmu::set_current_root(caller_task.addr_space.root_ppn); + return_sp = caller_task.tf.regs[REG_SP]; + logf!( + "breakpoint return: caller=%d pc=0x%x ra=0x%x sp=0x%x", + caller_idx as u32, + caller_task.tf.pc, + caller_task.tf.regs[REG_RA], + caller_task.tf.regs[REG_SP] + ); + } else { + panic!("breakpoint trap: caller task missing"); + } + // Mark the caller as the current task after the handoff. + *CURRENT_TASK.get_mut() = caller_idx; + } + let mut sstatus = read_sstatus(); + // Set SPP so sret returns to the correct privilege level. + if caller_idx == KERNEL_TASK_SLOT { + // Return to supervisor when the caller is the kernel task. + sstatus |= SSTATUS_SPP; + return_kind = 1; + } else { + // Clear SPP to return to user mode for user callers. + sstatus &= !SSTATUS_SPP; + return_kind = 0; + } + unsafe { asm!("csrw sstatus, {0}", in(reg) sstatus); } + } + _ => log!("unhandled trap"), + } + (return_sp, return_kind) +} + +#[unsafe(no_mangle)] +/// Restore the kernel address-space root for traps arriving from user mode. +extern "C" fn ensure_kernel_root_for_trap() { + if read_sstatus() & SSTATUS_SPP != 0 { + return; + } + let kernel_root = unsafe { + TASKS + .get_mut() + .get(KERNEL_TASK_SLOT) + .map(|task| task.addr_space.root_ppn) + .unwrap_or_else(mmu::current_root) + }; + mmu::set_current_root(kernel_root); +} + +#[inline(always)] +fn read_scause() -> usize { + let value: usize; + unsafe { asm!("csrr {0}, scause", out(reg) value); } + value +} + +#[inline(always)] +fn read_satp() -> u32 { + let value: u32; + unsafe { asm!("csrr {0}, satp", out(reg) value); } + value +} + +#[inline(always)] +fn read_sstatus() -> u32 { + let value: u32; + unsafe { asm!("csrr {0}, sstatus", out(reg) value); } + value +} + +fn read_task_result(task: &Task) -> Option { + let result_bytes = + read_user_bytes(task.addr_space.root_ppn, RESULT_ADDR, MAX_RESULT_SIZE)?; + if result_bytes.len() < 9 { + return None; + } + let success = result_bytes[0] != 0; + let error_code = u32::from_le_bytes(result_bytes[1..5].try_into().ok()?); + let data_len = u32::from_le_bytes(result_bytes[5..9].try_into().ok()?); + let data_len = (data_len as usize).min(RESULT_DATA_SIZE); + if result_bytes.len() < 9 + data_len { + return None; + } + let mut data = [0u8; RESULT_DATA_SIZE]; + data[..data_len].copy_from_slice(&result_bytes[9..9 + data_len]); + Some(VmResult { + success, + error_code, + data_len: data_len as u32, + data, + }) +} + +fn log_task_result(result: &VmResult) { + let data_len = (result.data_len as usize).min(RESULT_DATA_SIZE); + logf!( + "program result: success=%d error=%d data_len=%d", + result.success as u32, + result.error_code, + data_len as u32 + ); + if data_len > 0 { + log!("program result data: %b", &result.data[..data_len]); + } +} + +fn write_result_to_caller(caller_task: &mut Task, result: &VmResult) -> Option { + let addr = alloc_in_task(caller_task, MAX_RESULT_SIZE as u32, 4)?; + let mut buf = [0u8; MAX_RESULT_SIZE]; + buf[0] = result.success as u8; + buf[1..5].copy_from_slice(&result.error_code.to_le_bytes()); + buf[5..9].copy_from_slice(&result.data_len.to_le_bytes()); + let data_len = (result.data_len as usize).min(RESULT_DATA_SIZE); + if data_len > 0 { + buf[9..9 + data_len].copy_from_slice(&result.data[..data_len]); + } + if !mmu::copy(caller_task.addr_space.root_ppn, addr, &buf) { + return None; + } + Some(addr) +} + +#[inline(always)] +fn read_stval() -> usize { + let value: usize; + unsafe { asm!("csrr {0}, stval", out(reg) value); } + value +} diff --git a/crates/kernel/src/trap/restore_trap_frame.rs b/crates/kernel/src/trap/restore_trap_frame.rs new file mode 100644 index 0000000..67e9e54 --- /dev/null +++ b/crates/kernel/src/trap/restore_trap_frame.rs @@ -0,0 +1,54 @@ +use super::{return_from_trap, TRAP_FRAME_BYTES}; + +#[unsafe(naked)] +pub(super) unsafe extern "C" fn restore_trap_frame( + return_sp: u32, /* used via a0 reg */ + frame_ptr: *const u32, +) -> ! { + core::arch::naked_asm!( + // Restore sepc and registers using the provided trap-frame base (a1). + "lw t1, 128(a1)", + "csrw sepc, t1", + "beqz a2, 1f", + "csrw sscratch, a0", // kernel return: keep kernel sp for subsequent traps + "j 2f", + "1:", + "addi t1, a1, {frame_bytes}", + "csrw sscratch, t1", // user return: stash kernel sp for the next trap + "2:", + "lw ra, 4(a1)", + "lw gp, 12(a1)", + "lw tp, 16(a1)", + "lw t0, 20(a1)", // user satp from trap trampoline + "lw t1, 24(a1)", + "lw t2, 28(a1)", + "lw s0, 32(a1)", + "lw s1, 36(a1)", + "lw a2, 48(a1)", + "lw a3, 52(a1)", + "lw a4, 56(a1)", + "lw a5, 60(a1)", + "lw a6, 64(a1)", + "lw a7, 68(a1)", + "lw s2, 72(a1)", + "lw s3, 76(a1)", + "lw s4, 80(a1)", + "lw s5, 84(a1)", + "lw s6, 88(a1)", + "lw s7, 92(a1)", + "lw s8, 96(a1)", + "lw s9, 100(a1)", + "lw s10, 104(a1)", + "lw s11, 108(a1)", + "lw t3, 112(a1)", + "lw t4, 116(a1)", + "lw t5, 120(a1)", + "lw t6, 124(a1)", + "mv sp, a0", // restore caller-selected sp (user or kernel), not sscratch + "lw a0, 40(a1)", + "lw a1, 44(a1)", + "j {return}", + return = sym return_from_trap, + frame_bytes = const TRAP_FRAME_BYTES, + ); +} diff --git a/crates/kernel/src/trap/save_trap_frame.rs b/crates/kernel/src/trap/save_trap_frame.rs new file mode 100644 index 0000000..80e07a6 --- /dev/null +++ b/crates/kernel/src/trap/save_trap_frame.rs @@ -0,0 +1,43 @@ +#[unsafe(naked)] +pub(super) unsafe extern "C" fn save_trap_frame() -> ! { + core::arch::naked_asm!( + // Save all GPRs + sepc (PC). User sp lives in sscratch after the swap. + "sw zero, 0(sp)", // x0 + "sw ra, 4(sp)", // x1 + "sw t1, 24(sp)", // x6 (save before clobber) + "csrr t1, sscratch", // user sp + "sw t1, 8(sp)", // x2 + "sw gp, 12(sp)", // x3 + "sw tp, 16(sp)", // x4 + "sw t0, 20(sp)", // x5 (user satp from trap trampoline) + "sw t2, 28(sp)", // x7 + "sw s0, 32(sp)", // x8 + "sw s1, 36(sp)", // x9 + "sw a0, 40(sp)", // x10 + "sw a1, 44(sp)", // x11 + "sw a2, 48(sp)", // x12 + "sw a3, 52(sp)", // x13 + "sw a4, 56(sp)", // x14 + "sw a5, 60(sp)", // x15 + "sw a6, 64(sp)", // x16 + "sw a7, 68(sp)", // x17 + "sw s2, 72(sp)", // x18 + "sw s3, 76(sp)", // x19 + "sw s4, 80(sp)", // x20 + "sw s5, 84(sp)", // x21 + "sw s6, 88(sp)", // x22 + "sw s7, 92(sp)", // x23 + "sw s8, 96(sp)", // x24 + "sw s9, 100(sp)", // x25 + "sw s10, 104(sp)", // x26 + "sw s11, 108(sp)", // x27 + "sw t3, 112(sp)", // x28 + "sw t4, 116(sp)", // x29 + "sw t5, 120(sp)", // x30 + "sw t6, 124(sp)", // x31 + "csrr t1, sepc", + "sw t1, 128(sp)", // pc + "mv a0, sp", // return saved-area pointer in a0 + "ret", + ); +} diff --git a/crates/kernel/src/user_program.rs b/crates/kernel/src/user_program.rs new file mode 100644 index 0000000..c3ec9da --- /dev/null +++ b/crates/kernel/src/user_program.rs @@ -0,0 +1,80 @@ +extern crate alloc; + +use alloc::format; +use clibc::logf; +use state::State; +use types::address::Address; + +use crate::global::{CODE_SIZE_LIMIT, RO_DATA_SIZE_LIMIT, STATE}; + +pub struct ProgramImage<'a> { + pub code: &'a [u8], + pub entry_off: u32, +} + +// Load a program image from STATE, validate it, and pass a borrowed view to a caller. +// This centralizes code lookup, contract checks, and entry offset calculation. +pub fn with_program_image( + to: &Address, + f: impl FnOnce(ProgramImage<'_>) -> Option, +) -> Option { + // Fetch the account from state (or log and bail if it is missing). + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = match state.get_account(to) { + Some(acc) => acc, + None => { + logf!( + "%s", + display: format!("Program call failed: account {} does not exist", to) + ); + return None; + } + }; + + // Ensure the target is a contract; non-contract accounts cannot be executed. + if !account.is_contract { + logf!( + "%s", + display: format!( + "Program call failed: target {} is not a contract (code_len={})", + to, + account.code.len() + ) + ); + return None; + } + + // Find the first non-zero byte to infer the entry offset and log code stats. + let first_nz = account + .code + .iter() + .position(|&b| b != 0) + .unwrap_or(account.code.len()); + let nz_count = account.code.iter().filter(|&&b| b != 0).count(); + logf!( + "%s", + display: format!( + "Program code stats: len={} first_nz={} nz_count={}", + account.code.len(), + first_nz, + nz_count + ) + ); + + // Enforce the code size limit to prevent oversized binaries. + let code_len = account.code.len(); + let max = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT; + if code_len > max { + panic!( + "❌ Program call rejected: code size ({}) exceeds limit ({})", + code_len, max + ); + } + + // Provide the borrowed code slice and entry offset to the caller. + let entry_off = first_nz as u32; + f(ProgramImage { + code: &account.code, + entry_off, + }) +} diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml deleted file mode 100644 index 3612d98..0000000 --- a/crates/program/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "program" -version = "0.1.0" -edition = "2024" - -[dependencies] -types = { path = "../types" } diff --git a/crates/program/src/panic.rs b/crates/program/src/panic.rs deleted file mode 100644 index a64ca43..0000000 --- a/crates/program/src/panic.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::logf; - -#[cfg(target_arch = "riscv32")] -pub fn vm_panic(msg: &[u8]) -> ! { - unsafe { - core::arch::asm!( - "li a7, 3", // syscall: panic - "ecall", - in("a0") msg.as_ptr(), - in("a1") msg.len(), - ); - core::arch::asm!("ebreak", options(nomem, nostack)); - } - loop {} -} - -#[cfg(target_arch = "riscv32")] -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - if let Some(s) = info.message().as_str() { - vm_panic(s.as_bytes()); - } else { - vm_panic(b"panic occurred (non-str message)"); - } -} - -#[cfg(not(target_arch = "riscv32"))] -pub fn vm_panic(msg: &[u8]) -> ! { - panic!("vm_panic: {}", core::str::from_utf8(msg).unwrap_or("")); -} diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index bb0f501..db4a34d 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -5,8 +5,5 @@ authors = ["Alon Muroch "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -hex = "0.4" -storage = { path = "../storage" } # adjust path as needed -types = { path = "../types" } # adjust path as needed \ No newline at end of file +types = { path = "../types" } # adjust path as needed diff --git a/crates/state/src/account.rs b/crates/state/src/account.rs index 2da4603..4f290a5 100644 --- a/crates/state/src/account.rs +++ b/crates/state/src/account.rs @@ -1,4 +1,6 @@ use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; #[derive(Clone, Debug)] pub struct Account { @@ -8,4 +10,4 @@ pub struct Account { pub is_contract: bool, pub storage: BTreeMap>, -} \ No newline at end of file +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index f144646..0a2707d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,3 +1,7 @@ +#![no_std] + +extern crate alloc; + pub mod types; pub mod account; pub mod state; @@ -5,5 +9,3 @@ pub mod state; pub use types::*; pub use account::*; pub use state::*; - -extern crate alloc; \ No newline at end of file diff --git a/crates/state/src/state.rs b/crates/state/src/state.rs index b44ef30..c2adcb5 100644 --- a/crates/state/src/state.rs +++ b/crates/state/src/state.rs @@ -1,10 +1,8 @@ -use std::rc::Rc; -use std::collections::HashMap; -use storage::Storage; -use crate::{Account}; -use types::address::Address; -use hex::encode as hex_encode; use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use crate::Account; +use types::address::Address; /// Represents the global state of the blockchain virtual machine. /// @@ -47,7 +45,7 @@ pub struct State { /// EDUCATIONAL: This is the core data structure that represents the /// entire blockchain state. Each entry contains an account with its /// balance, code, storage, and other metadata. - pub accounts: HashMap, + pub accounts: BTreeMap, } impl State { @@ -60,20 +58,7 @@ impl State { /// USAGE: Typically called when starting a new blockchain or when /// resetting the state for testing purposes. pub fn new() -> Self { - Self { accounts: HashMap::new() } - } - - /// Constructs a State from an existing Storage instance. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how state can be reconstructed - /// from persistent storage. In real blockchains, the state is often - /// stored on disk and loaded into memory when needed. - /// - /// NOTE: This is currently a placeholder implementation that always - /// returns an empty state. In a real system, this would deserialize - /// the state from the provided storage. - pub fn new_from_storage(_storage: Rc) -> Self { - Self { accounts: HashMap::new() } + Self { accounts: BTreeMap::new() } } /// Retrieves an account by address (immutable reference). @@ -93,6 +78,14 @@ impl State { self.accounts.get(addr) } + /// Returns the current balance for an address (0 if missing). + pub fn balance_of(&self, addr: &Address) -> u128 { + self.accounts + .get(addr) + .map(|acc| acc.balance) + .unwrap_or(0) + } + /// Retrieves an account by address (mutable reference), creating it if it doesn't exist. /// /// EDUCATIONAL PURPOSE: This demonstrates account creation on-demand. @@ -135,6 +128,36 @@ impl State { }) } + /// Transfers native balance between accounts. Returns false on insufficient funds or overflow. + pub fn transfer(&mut self, from: &Address, to: &Address, value: u64) -> bool { + let amount = value as u128; + let from_balance = match self.get_account(from) { + Some(account) => account.balance, + None => return false, + }; + if from_balance < amount { + return false; + } + if from == to { + return true; + } + let to_balance = self.balance_of(to); + let new_to_balance = match to_balance.checked_add(amount) { + Some(balance) => balance, + None => return false, + }; + + { + let from_account = self.get_account_mut(from); + from_account.balance = from_balance - amount; + } + { + let to_account = self.get_account_mut(to); + to_account.balance = new_to_balance; + } + true + } + /// Checks if an address corresponds to a contract account. /// /// EDUCATIONAL PURPOSE: This demonstrates how to distinguish between @@ -154,135 +177,161 @@ impl State { return true; } - /// Deploys a contract to a specific address. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract deployment. - /// When a contract is deployed, it creates or updates an account with - /// the contract's bytecode and marks it as a contract account. - /// - /// DEPLOYMENT PROCESS: - /// 1. Get or create the account at the specified address - /// 2. Set the account's code to the provided bytecode - /// 3. Mark the account as a contract - /// - /// SECURITY: In real systems, contract deployment would include - /// additional checks like code validation, gas limits, etc. - /// - /// PARAMETERS: - /// - addr: The address where the contract should be deployed - /// - code: The bytecode of the contract to deploy - pub fn deploy_contract(&mut self, addr: Address, code: Vec) { - // EDUCATIONAL: Get or create the account at the specified address - let acc = self.accounts.entry(addr).or_insert_with(|| Account { - nonce: 0, // No transactions yet - balance: 0, // No initial balance - code: Vec::new(), // No code initially - is_contract: false, // Not a contract initially - storage: BTreeMap::new(), // Empty storage - }); - - // EDUCATIONAL: Set the contract code and mark as contract - acc.code = code; // Deploy the bytecode - acc.is_contract = true; // Mark as contract account + /// Encode state into a byte buffer for guest consumption. + pub fn encode(&self) -> alloc::vec::Vec { + let len = self.encoded_len(); + let mut out = alloc::vec![0u8; len]; + let _ = self.encode_into(&mut out); + out } - /// Prints a human-readable representation of the current state. - /// - /// EDUCATIONAL PURPOSE: This demonstrates state inspection and debugging. - /// Being able to visualize the blockchain state is crucial for development, - /// testing, and understanding how transactions affect the system. - /// - /// OUTPUT FORMAT: Shows each account with its: - /// - Address (in hexadecimal) - /// - Balance - /// - Nonce (transaction count) - /// - Contract status - /// - Code size - /// - Storage contents - /// - /// USAGE: Useful for debugging, testing, and educational demonstrations. - pub fn pretty_print(&self) { - println!("--- State Dump ---"); + /// Returns the byte length of the encoded state. + pub fn encoded_len(&self) -> usize { + let mut total = 4usize; // account count for (addr, acc) in &self.accounts { - // EDUCATIONAL: Display account address in hexadecimal format - println!(" πŸ”‘ Address: 0x{}", hex_encode(addr.0)); - - // EDUCATIONAL: Display account metadata - println!(" - Balance: {}", acc.balance); - println!(" - Nonce: {}", acc.nonce); - println!(" - Is contract?: {}", acc.is_contract); - println!(" - Code size: {} bytes", acc.code.len()); - - // EDUCATIONAL: Display storage contents - println!(" - Storage:"); - for (key, value) in &acc.storage { - // EDUCATIONAL: Convert storage values to hexadecimal for readability - let value_hex: Vec = value.iter().map(|b| format!("{:02x}", b)).collect(); - - // Parse the key as "domain:key" format - if let Some((domain, key_part)) = Self::parse_domain_key(key) { - if domain == "P" { - // For persistent storage, treat key as ASCII - if let Ok(ascii_key) = String::from_utf8(hex::decode(&key_part).unwrap_or_default()) { - println!(" Key: {}:{} | Value ({} bytes): {}", domain, ascii_key, value.len(), value_hex.join(" ")); - } else { - println!(" Key: {}:{} | Value ({} bytes): {}", domain, key_part, value.len(), value_hex.join(" ")); - } - } else { - // For storage maps, treat domain as ASCII and key as hex - println!(" Key: {}:{} | Value ({} bytes): {}", domain, key_part, value.len(), value_hex.join(" ")); - } - } else { - // Fall back to showing the raw key - println!(" Key: {:<20} | Value ({} bytes): {}", key, value.len(), value_hex.join(" ")); - } + let mut acc_len = 0usize; + acc_len = acc_len.saturating_add(addr.0.len()); + acc_len = acc_len.saturating_add(16); // balance + acc_len = acc_len.saturating_add(8); // nonce + acc_len = acc_len.saturating_add(1); // is_contract + acc_len = acc_len.saturating_add(4); // code len + acc_len = acc_len.saturating_add(acc.code.len()); + acc_len = acc_len.saturating_add(4); // storage len + for (k, v) in &acc.storage { + acc_len = acc_len.saturating_add(4); // key len + acc_len = acc_len.saturating_add(k.as_bytes().len()); + acc_len = acc_len.saturating_add(4); // val len + acc_len = acc_len.saturating_add(v.len()); } - println!(); + total = total.saturating_add(acc_len); } - println!("--------------------"); + total } - - /// Parses a storage key in "domain:key" format to extract domain and key components. - /// - /// Storage keys are formatted as: "domain:key" - /// where domain is like "P" or "Balances" and key is hex-encoded - fn parse_domain_key(key: &str) -> Option<(String, String)> { - // Find the first colon to separate domain and key - if let Some(colon_pos) = key.find(':') { - let domain = key[..colon_pos].to_string(); - let key_part = key[colon_pos + 1..].to_string(); - return Some((domain, key_part)); + + /// Encode state into a provided buffer. Returns bytes written on success. + pub fn encode_into(&self, out: &mut [u8]) -> Option { + let mut cursor = 0usize; + let write = |buf: &mut [u8], cursor: &mut usize, bytes: &[u8]| -> Option<()> { + if *cursor + bytes.len() > buf.len() { + return None; + } + buf[*cursor..*cursor + bytes.len()].copy_from_slice(bytes); + *cursor += bytes.len(); + Some(()) + }; + + let count = self.accounts.len() as u32; + write(out, &mut cursor, &count.to_le_bytes())?; + + for (addr, acc) in &self.accounts { + write(out, &mut cursor, &addr.0)?; + write(out, &mut cursor, &acc.balance.to_le_bytes())?; + write(out, &mut cursor, &acc.nonce.to_le_bytes())?; + write(out, &mut cursor, &[acc.is_contract as u8])?; + let code_len = acc.code.len() as u32; + write(out, &mut cursor, &code_len.to_le_bytes())?; + write(out, &mut cursor, &acc.code)?; + + let storage_len = acc.storage.len() as u32; + write(out, &mut cursor, &storage_len.to_le_bytes())?; + for (k, v) in &acc.storage { + let key_len = k.as_bytes().len() as u32; + write(out, &mut cursor, &key_len.to_le_bytes())?; + write(out, &mut cursor, k.as_bytes())?; + let val_len = v.len() as u32; + write(out, &mut cursor, &val_len.to_le_bytes())?; + write(out, &mut cursor, v)?; + } } - - None + + Some(cursor) } - - /// Parses a storage map key to extract address and domain components. - /// - /// Storage map keys are formatted as: [address_bytes][domain] - /// where address_bytes is 20 bytes and domain is like "-Balances" - fn parse_storage_map_key(key: &str) -> Option<(String, String)> { - // Check if the key is long enough to contain an address (20 bytes = 40 hex chars) - if key.len() < 40 { - return None; - } - - // Try to parse the first 40 characters as a hex address - if let Ok(address_bytes) = hex::decode(&key[..40]) { - if address_bytes.len() == 20 { - // Convert to proper address format - let address = format!("0x{}", &key[..40]); - - // Parse the domain (remaining hex characters) - let domain_hex = &key[40..]; - if let Ok(domain_bytes) = hex::decode(domain_hex) { - if let Ok(domain_str) = String::from_utf8(domain_bytes) { - return Some((address, domain_str)); - } - } + + /// Decode state produced by `encode`. + pub fn decode(bytes: &[u8]) -> Option { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > bytes.len() { + return None; } + let slice = &bytes[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let count = { + let raw = read(4)?; + let mut buf = [0u8; 4]; + buf.copy_from_slice(raw); + u32::from_le_bytes(buf) as usize + }; + + let mut accounts = BTreeMap::new(); + for _ in 0..count { + let mut addr = [0u8; 20]; + addr.copy_from_slice(read(20)?); + + let balance = { + let mut buf = [0u8; 16]; + buf.copy_from_slice(read(16)?); + u128::from_le_bytes(buf) + }; + + let nonce = { + let mut buf = [0u8; 8]; + buf.copy_from_slice(read(8)?); + u64::from_le_bytes(buf) + }; + + let is_contract = read(1)?.first().copied()? != 0; + + let code_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let code = read(code_len)?.to_vec(); + + let storage_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let mut storage = BTreeMap::new(); + for _ in 0..storage_len { + let key_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let key = { + let raw = read(key_len)?; + core::str::from_utf8(raw).ok()?.to_string() + }; + + let val_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let val = read(val_len)?.to_vec(); + + storage.insert(key, val); + } + + accounts.insert( + Address(addr), + Account { + nonce, + balance, + code, + is_contract, + storage, + }, + ); } - - None + + Some(Self { accounts }) } + } diff --git a/crates/state/tests/state_codec.rs b/crates/state/tests/state_codec.rs new file mode 100644 index 0000000..0b352df --- /dev/null +++ b/crates/state/tests/state_codec.rs @@ -0,0 +1,76 @@ +use std::collections::BTreeMap; +use std::string::String; +use std::vec::Vec; + +use state::{Account, State}; +use types::address::Address; + +fn assert_account_eq(expected: &Account, actual: &Account) { + assert_eq!(expected.nonce, actual.nonce); + assert_eq!(expected.balance, actual.balance); + assert_eq!(expected.code, actual.code); + assert_eq!(expected.is_contract, actual.is_contract); + assert_eq!(expected.storage, actual.storage); +} + +#[test] +fn encode_decode_empty_state() { + let state = State::new(); + let encoded = state.encode(); + let decoded = State::decode(&encoded).expect("decode empty state"); + assert!(decoded.accounts.is_empty()); +} + +#[test] +fn encode_decode_with_account_and_storage() { + let mut state = State::new(); + let addr = Address([ + 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, + ]); + let mut storage = BTreeMap::new(); + storage.insert(String::from("key"), vec![0xde, 0xad, 0xbe, 0xef]); + storage.insert(String::from("empty"), Vec::new()); + let account = Account { + nonce: 42, + balance: 123_456_789, + code: vec![0xaa, 0xbb, 0xcc], + is_contract: true, + storage, + }; + state.accounts.insert(addr, account.clone()); + + let encoded = state.encode(); + let decoded = State::decode(&encoded).expect("decode populated state"); + + let decoded_account = decoded.accounts.get(&addr).expect("account exists"); + assert_account_eq(&account, decoded_account); +} + +#[test] +fn decode_truncated_bytes_returns_none() { + let mut state = State::new(); + let addr = Address([0x11; 20]); + state.accounts.insert( + addr, + Account { + nonce: 1, + balance: 2, + code: vec![0x42], + is_contract: false, + storage: BTreeMap::new(), + }, + ); + let encoded = state.encode(); + let truncated = &encoded[..encoded.len().saturating_sub(1)]; + assert!(State::decode(truncated).is_none()); +} + +#[test] +fn decode_zero_count_header_returns_empty_state() { + let bytes = [0u8; 4]; + let decoded = State::decode(&bytes).expect("decode zero-count header"); + assert!(decoded.accounts.is_empty()); +} diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 2307694..6410daf 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,11 +1,14 @@ -// crates/storage/src/lib.rs +#![no_std] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; -use core::cell::RefCell; use alloc::collections::BTreeMap; -use alloc::vec::Vec; +use alloc::format; use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; /// Represents persistent storage for the blockchain virtual machine. /// @@ -159,13 +162,14 @@ impl Storage { /// /// OUTPUT FORMAT: Shows each key-value pair in storage, with the value /// displayed in hexadecimal format. + #[cfg(feature = "std")] pub fn dump(&self) { - println!("--- Storage Dump ---"); + std::println!("--- Storage Dump ---"); for (key, value) in self.map.borrow().iter() { let key_str = key; let value_hex: Vec = value.iter().map(|b| format!("{:02x}", b)).collect(); - println!("Key: {:<20} | Value ({} bytes): {}", key_str, value.len(), value_hex.join(" ")); + std::println!("Key: {:<20} | Value ({} bytes): {}", key_str, value.len(), value_hex.join(" ")); } - println!("--------------------"); + std::println!("--------------------"); } } diff --git a/crates/types/src/address.rs b/crates/types/src/address.rs index f6ee483..c7e2760 100644 --- a/crates/types/src/address.rs +++ b/crates/types/src/address.rs @@ -2,7 +2,9 @@ use core::fmt; use crate::O; use crate::SerializeField; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub const ADDRESS_LEN: usize = 20; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(C)] pub struct Address(pub [u8; 20]); @@ -56,4 +58,4 @@ impl SerializeField for Address { panic!("Buffer overflow in Address serialization"); } } -} \ No newline at end of file +} diff --git a/crates/types/src/boot.rs b/crates/types/src/boot.rs new file mode 100644 index 0000000..3e8028c --- /dev/null +++ b/crates/types/src/boot.rs @@ -0,0 +1,49 @@ +//! Boot-time handoff structures shared between bootloader and kernel. +//! +//! These types live in `types` so both sides agree on layout without +//! introducing circular dependencies. + +/// Minimal boot information passed from the bootloader to the kernel. +/// +/// Fields are kept simple and `#[repr(C)]` so the bootloader can write this +/// structure into guest memory and the kernel can read it back verbatim. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct BootInfo { + /// Root page-table physical page number to load into satp. + pub root_ppn: u32, + /// Top of the kernel stack. + pub kstack_top: u32, + /// Next heap pointer for kernel allocations (virtual address). + pub heap_ptr: u32, + /// Total physical memory size in bytes. + pub memory_size: u32, + /// First free physical page number after bootloader allocations. + pub next_free_ppn: u32, + /// Base virtual address of the mapped VA window. + pub va_base: u32, + /// Size in bytes of the mapped VA window. + pub va_len: u32, +} + +impl BootInfo { + pub const fn new( + root_ppn: u32, + kstack_top: u32, + heap_ptr: u32, + memory_size: u32, + next_free_ppn: u32, + va_base: u32, + va_len: u32, + ) -> Self { + Self { + root_ppn, + kstack_top, + heap_ptr, + memory_size, + next_free_ppn, + va_base, + va_len, + } + } +} diff --git a/crates/types/src/kernel_result.rs b/crates/types/src/kernel_result.rs new file mode 100644 index 0000000..eff6edd --- /dev/null +++ b/crates/types/src/kernel_result.rs @@ -0,0 +1,14 @@ +//! Kernel-to-bootloader handoff header for serialized receipts. + +/// Pointer + length describing kernel-owned output buffers. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct KernelResult { + pub receipts_ptr: u32, + pub receipts_len: u32, + pub state_ptr: u32, + pub state_len: u32, +} + +/// Kernel VA where the handoff header is written. +pub const KERNEL_RESULT_ADDR: u32 = 0x100; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 4625de0..178b01b 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -1,7 +1,9 @@ #![no_std] +extern crate alloc; + pub mod address; -pub use address::Address; +pub use address::{Address, ADDRESS_LEN}; pub mod result; pub use result::Result; @@ -13,8 +15,23 @@ pub use o::*; // Allow `$crate::O` in macros pub mod primitives; pub use primitives::*; +pub mod transaction; +pub use transaction::*; + +pub mod receipt; +pub use receipt::TransactionReceipt; + +pub mod kernel_result; +pub use kernel_result::KernelResult; + +pub mod boot; +pub use boot::BootInfo; + +pub mod mmu; +pub use mmu::*; + // used for serialization pub trait SerializeField { /// Appends `self` into `buf` at `*offset`, advancing the offset. fn serialize_field(&self, buf: &mut [u8], offset: &mut usize); -} \ No newline at end of file +} diff --git a/crates/types/src/mmu.rs b/crates/types/src/mmu.rs new file mode 100644 index 0000000..0562b4c --- /dev/null +++ b/crates/types/src/mmu.rs @@ -0,0 +1,258 @@ +#![allow(dead_code)] + +use core::convert::TryFrom; +use core::mem; + +/// Sv32 page size in bytes (4 KiB). +pub const SV32_PAGE_SIZE: usize = 4096; +/// Number of bits in a VPN index. +pub const SV32_VPN_MASK: u32 = 0x3ff; + +/// Sv32 PTE flag bits. +pub const SV32_PTE_V: u32 = 1 << 0; +pub const SV32_PTE_R: u32 = 1 << 1; +pub const SV32_PTE_W: u32 = 1 << 2; +pub const SV32_PTE_X: u32 = 1 << 3; +pub const SV32_PTE_U: u32 = 1 << 4; + +/// Sv32 satp PPN mask (bits 21:0). Mode bits are ignored in this emulator. +pub const SV32_SATP_PPN_MASK: u32 = 0x003f_ffff; + +/// Virtual base used to directly map all guest physical memory for the kernel. +/// This keeps physical page-table frames accessible even after paging is enabled. +pub const SV32_DIRECT_MAP_BASE: u32 = 0x4000_0000; + +/// Simple permission descriptor for Sv32 mappings. +#[derive(Clone, Copy, Debug)] +pub struct Sv32PagePerms { + /// Allow load access (read data from this page). + pub read: bool, + /// Allow store access (write data to this page). + pub write: bool, + /// Allow instruction fetches from this page. + pub exec: bool, + /// Mark the page as user-accessible (U bit set). + pub user: bool, +} + +impl Sv32PagePerms { + pub const fn new(read: bool, write: bool, exec: bool, user: bool) -> Self { + Self { + read, + write, + exec, + user, + } + } + + pub const fn user_rwx() -> Self { + Self::new(true, true, true, true) + } + + pub const fn kernel_rw() -> Self { + Self::new(true, true, false, false) + } + + pub const fn kernel_rwx() -> Self { + Self::new(true, true, true, false) + } + + fn to_pte_flags(self) -> u32 { + let mut flags = SV32_PTE_V; + if self.read { + flags |= SV32_PTE_R; + } + if self.write { + flags |= SV32_PTE_W; + } + if self.exec { + flags |= SV32_PTE_X; + } + if self.user { + flags |= SV32_PTE_U; + } + flags + } +} + +/// Abstraction for Sv32 page-table manipulation. +/// +/// Implementations provide raw PTE reads/writes at physical addresses, frame +/// allocation, and zeroing. Mapping helpers below drive the common Sv32 walk +/// for both the bootloader and kernel. +pub trait Sv32PageTable { + fn page_size(&self) -> usize { + SV32_PAGE_SIZE + } + + fn read_pte(&self, phys_addr: usize) -> Option; + fn write_pte(&self, phys_addr: usize, val: u32); + fn alloc_frame(&self) -> Option; + fn zero_frame(&self, ppn: u32); +} + +/// Map a virtual range by allocating fresh physical frames for leaves. +/// +/// Returns false on allocation/overflow failures or unsupported superpage cases. +pub fn map_allocating( + pt: &T, + root_ppn: u32, + va_start: u32, + len: usize, + perms: Sv32PagePerms, +) -> bool { + map_range_internal(pt, root_ppn, va_start, len, perms, LeafStrategy::Allocate) +} + +/// Map a virtual range to an existing physical range (no leaf allocation). +/// +/// `phys_start` must be page aligned. Returns false on failure. +pub fn map_to_physical( + pt: &T, + root_ppn: u32, + va_start: u32, + phys_start: u32, + len: usize, + perms: Sv32PagePerms, +) -> bool { + if phys_start as usize % pt.page_size() != 0 { + return false; + } + map_range_internal( + pt, + root_ppn, + va_start, + len, + perms, + LeafStrategy::PhysCursor { + next_phys: phys_start, + }, + ) +} + +#[derive(Clone, Copy)] +enum LeafStrategy { + Allocate, + PhysCursor { next_phys: u32 }, +} + +fn map_range_internal( + pt: &T, + root_ppn: u32, + va_start: u32, + len: usize, + perms: Sv32PagePerms, + mut strategy: LeafStrategy, +) -> bool { + if len == 0 { + return true; + } + + let page_size = pt.page_size(); + let start = align_down(va_start as usize, page_size); + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up(v, page_size), + None => return false, + }; + + let mut va = start as u32; + while (va as usize) < end { + let phys_override = match &mut strategy { + LeafStrategy::Allocate => None, + LeafStrategy::PhysCursor { next_phys } => { + let phys = *next_phys; + *next_phys = next_phys.wrapping_add(page_size as u32); + Some(phys) + } + }; + + if !map_page(pt, root_ppn, va, perms, phys_override) { + return false; + } + va = va.wrapping_add(page_size as u32); + } + true +} + +fn map_page( + pt: &T, + root_ppn: u32, + va: u32, + perms: Sv32PagePerms, + phys_override: Option, +) -> bool { + let page_size = pt.page_size(); + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let root_base = match (root_ppn as usize).checked_mul(page_size) { + Some(base) => base, + None => return false, + }; + let l1_entry_addr = root_base + vpn1 as usize * mem::size_of::(); + let mut l1_pte = match pt.read_pte(l1_entry_addr) { + Some(pte) => pte, + None => return false, + }; + + if l1_pte & SV32_PTE_V == 0 { + let l2 = match pt.alloc_frame() { + Some(ppn) => ppn, + None => return false, + }; + pt.zero_frame(l2); + l1_pte = (l2 << 10) | SV32_PTE_V; + pt.write_pte(l1_entry_addr, l1_pte); + } else if l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + // Superpages are not supported. + return false; + } + + let l2_base = match usize::try_from(l1_pte >> 10) + .ok() + .and_then(|ppn| ppn.checked_mul(page_size)) + { + Some(base) => base, + None => return false, + }; + let l2_entry_addr = l2_base + vpn0 as usize * mem::size_of::(); + + let existing = pt.read_pte(l2_entry_addr).unwrap_or(0); + let existing_valid = (existing & SV32_PTE_V) != 0; + let existing_ppn = existing >> 10; + + let leaf_ppn = match (existing_valid, phys_override) { + (true, Some(phys)) => { + if (phys as usize) % page_size != 0 { + return false; + } + phys / page_size as u32 + } + (true, None) => existing_ppn, + (false, Some(phys)) => { + if (phys as usize) % page_size != 0 { + return false; + } + phys / page_size as u32 + } + (false, None) => match pt.alloc_frame() { + Some(ppn) => { + pt.zero_frame(ppn); + ppn + } + None => return false, + }, + }; + + let leaf_pte = (leaf_ppn << 10) | perms.to_pte_flags(); + pt.write_pte(l2_entry_addr, leaf_pte); + true +} + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +const fn align_down(val: usize, align: usize) -> usize { + val & !(align - 1) +} diff --git a/crates/types/src/receipt.rs b/crates/types/src/receipt.rs new file mode 100644 index 0000000..d8a4a58 --- /dev/null +++ b/crates/types/src/receipt.rs @@ -0,0 +1,212 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::convert::TryInto; +use core::fmt; + +use crate::result::Result; +use crate::transaction::Transaction; + +/// Represents the result of a transaction execution. +#[derive(Debug, Clone)] +pub struct TransactionReceipt { + /// Hash of the transaction. + pub tx: Transaction, + + /// Result status and optional data. + pub result: Result, + + /// List of log entries generated during execution. + pub events: Vec>, +} + +impl TransactionReceipt { + /// Creates a new TransactionReceipt. + pub fn new(tx: Transaction, result: Result) -> Self { + TransactionReceipt { + tx, + result, + events: Vec::new(), + } + } + + /// Adds an event to the receipt. + pub fn add_event(&mut self, event: Vec) -> &TransactionReceipt { + self.events.push(event); + self + } + + /// Optionally add multiple events at once. + pub fn set_events(mut self, events: Vec>) -> Self { + self.events = events; + self + } + + /// Encode this receipt into a flat little-endian buffer. + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + out.push(self.tx.tx_type as u8); + out.extend_from_slice(&self.tx.to.0); + out.extend_from_slice(&self.tx.from.0); + out.extend_from_slice(&(self.tx.data.len() as u32).to_le_bytes()); + out.extend_from_slice(&self.tx.data); + out.extend_from_slice(&self.tx.value.to_le_bytes()); + out.extend_from_slice(&self.tx.nonce.to_le_bytes()); + + out.push(self.result.success as u8); + out.extend_from_slice(&self.result.error_code.to_le_bytes()); + out.extend_from_slice(&self.result.data_len.to_le_bytes()); + let data_len = self.result.data_len as usize; + out.extend_from_slice(&self.result.data[..data_len.min(self.result.data.len())]); + + out.extend_from_slice(&(self.events.len() as u32).to_le_bytes()); + for event in &self.events { + out.extend_from_slice(&(event.len() as u32).to_le_bytes()); + out.extend_from_slice(event); + } + + out + } + + /// Decode a receipt from a buffer, returning the receipt and bytes consumed. + pub fn decode(encoded: &[u8]) -> Option<(Self, usize)> { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let tx_type = *read(1)?.first()?; + let tx_type = crate::transaction::TransactionType::from_u8(tx_type)?; + + let mut to = [0u8; 20]; + to.copy_from_slice(read(20)?); + let mut from = [0u8; 20]; + from.copy_from_slice(read(20)?); + + let data_len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let data = read(data_len)?.to_vec(); + + let value = + u64::from_le_bytes(read(8)?.try_into().ok()?); + let nonce = + u64::from_le_bytes(read(8)?.try_into().ok()?); + + let success = *read(1)?.first()? != 0; + let error_code = + u32::from_le_bytes(read(4)?.try_into().ok()?); + let result_data_len = + u32::from_le_bytes(read(4)?.try_into().ok()?); + let result_len = result_data_len as usize; + let result_data = read(result_len)?; + + let mut data_buf = [0u8; crate::result::RESULT_DATA_SIZE]; + let copy_len = result_len.min(data_buf.len()); + data_buf[..copy_len].copy_from_slice(&result_data[..copy_len]); + + let mut result = Result { + success, + error_code, + data_len: result_data_len, + data: data_buf, + }; + if result.data_len as usize > crate::result::RESULT_DATA_SIZE { + result.data_len = crate::result::RESULT_DATA_SIZE as u32; + } + + let event_count = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let mut events = Vec::with_capacity(event_count); + for _ in 0..event_count { + let len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let bytes = read(len)?.to_vec(); + events.push(bytes); + } + + let tx = Transaction { + tx_type, + to: crate::address::Address(to), + from: crate::address::Address(from), + data, + value, + nonce, + }; + + Some(( + TransactionReceipt { + tx, + result, + events, + }, + cursor, + )) + } + + /// Encode a receipts list with a count prefix and per-receipt length. + pub fn encode_list(receipts: &[TransactionReceipt]) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(receipts.len() as u32).to_le_bytes()); + for receipt in receipts { + let encoded = receipt.encode(); + out.extend_from_slice(&(encoded.len() as u32).to_le_bytes()); + out.extend_from_slice(&encoded); + } + out + } + + /// Decode a receipts list produced by `encode_list`. + pub fn decode_list(encoded: &[u8]) -> Option> { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + let count = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let mut receipts = Vec::with_capacity(count); + for _ in 0..count { + let len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let slice = read(len)?; + let (receipt, consumed) = TransactionReceipt::decode(slice)?; + if consumed != len { + return None; + } + receipts.push(receipt); + } + Some(receipts) + } +} + +impl fmt::Display for TransactionReceipt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "=== Transaction Receipt ===")?; + writeln!(f, "From: {:?}", self.tx.from)?; + writeln!(f, "To: {:?}", self.tx.to)?; + writeln!(f, "Result: {:?}", self.result)?; + writeln!(f, "Events:")?; + + for (i, event) in self.events.iter().enumerate() { + write!(f, " [{}] ", i)?; + for (j, byte) in event.iter().enumerate() { + if j > 0 { + write!(f, " ")?; + } + write!(f, "{:02x}", byte)?; + } + writeln!(f)?; + } + + Ok(()) + } +} diff --git a/crates/types/src/transaction.rs b/crates/types/src/transaction.rs new file mode 100644 index 0000000..f64ea65 --- /dev/null +++ b/crates/types/src/transaction.rs @@ -0,0 +1,126 @@ +use alloc::vec::Vec; +use core::convert::TryInto; + +use crate::address::Address; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransactionType { + /// Type 0 - Regular value transfer (not a contract) + Transfer = 0, + /// Type 1 - Account create with program data (contract deployment) + CreateAccount = 1, + /// Type 2 - Contract call (calling into existing code) + ProgramCall = 2, +} + +impl TransactionType { + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(TransactionType::Transfer), + 1 => Some(TransactionType::CreateAccount), + 2 => Some(TransactionType::ProgramCall), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct Transaction { + pub tx_type: TransactionType, // type of transaction + pub to: Address, // recipient address + pub from: Address, // sender public key/address + pub data: Vec, // input data + pub value: u64, // amount/value sent + pub nonce: u64, // transaction nonce +} + +/// Holds a set of transactions to be processed as a unit. +#[derive(Debug, Clone)] +pub struct TransactionBundle { + pub transactions: Vec, +} + +impl TransactionBundle { + pub fn new(transactions: Vec) -> Self { + TransactionBundle { transactions } + } + + pub fn add_transaction(&mut self, tx: Transaction) { + self.transactions.push(tx); + } + + pub fn len(&self) -> usize { + self.transactions.len() + } + + pub fn is_empty(&self) -> bool { + self.transactions.is_empty() + } + + /// Encode the bundle into a flat little-endian buffer that can be copied into guest memory. + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(self.transactions.len() as u32).to_le_bytes()); + + for tx in &self.transactions { + out.push(tx.tx_type as u8); + out.extend_from_slice(&tx.to.0); + out.extend_from_slice(&tx.from.0); + out.extend_from_slice(&(tx.data.len() as u32).to_le_bytes()); + out.extend_from_slice(&tx.data); + out.extend_from_slice(&tx.value.to_le_bytes()); + out.extend_from_slice(&tx.nonce.to_le_bytes()); + } + + out + } + + /// Decode a buffer produced by `encode` back into a bundle. + pub fn decode(encoded: &[u8]) -> Option { + let mut cursor = 0usize; + + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let tx_count_bytes = read(4)?; + let tx_count = u32::from_le_bytes(tx_count_bytes.try_into().ok()?) as usize; + let mut transactions = Vec::with_capacity(tx_count); + + for _ in 0..tx_count { + let tx_type_byte = *read(1)?.first()?; + let tx_type = TransactionType::from_u8(tx_type_byte)?; + + let mut to = [0u8; 20]; + to.copy_from_slice(read(20)?); + let mut from = [0u8; 20]; + from.copy_from_slice(read(20)?); + + let data_len_bytes = read(4)?; + let data_len = u32::from_le_bytes(data_len_bytes.try_into().ok()?) as usize; + let data = read(data_len)?.to_vec(); + + let value_bytes = read(8)?; + let value = u64::from_le_bytes(value_bytes.try_into().ok()?); + + let nonce_bytes = read(8)?; + let nonce = u64::from_le_bytes(nonce_bytes.try_into().ok()?); + + transactions.push(Transaction { + tx_type, + to: Address(to), + from: Address(from), + data, + value, + nonce, + }); + } + + Some(TransactionBundle { transactions }) + } +} diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 211bc7d..92d3d14 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] state = { path = "../state" } # adjust path as needed -storage = { path = "../storage" } # adjust path as needed types = { path = "../types" } # adjust path as needed [features] diff --git a/crates/vm/src/console.rs b/crates/vm/src/console.rs new file mode 100644 index 0000000..6e0a157 --- /dev/null +++ b/crates/vm/src/console.rs @@ -0,0 +1,229 @@ +use crate::cpu::PrivilegeMode; +use crate::memory::{Memory, VirtualAddress}; +use crate::metering::{MeterResult, Metering}; +use core::fmt::Write; +use std::cell::RefCell; +use std::rc::Rc; +use std::string::String; +use std::vec::Vec; + +pub const CONSOLE_WRITE_ID: u32 = 1000; + +enum Arg { + U32(u32), + F32(f32), + Char(char), + Str(String), + Bytes(Vec), +} + +pub fn console_write( + args: [u32; 6], + caller_mode: PrivilegeMode, + memory: Memory, + metering: &mut dyn Metering, + verbose_writer: &Option>>, +) -> u32 { + let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; + let payload_len = fmt_len.saturating_add(arg_len) as usize; + if matches!( + metering.on_syscall_data(CONSOLE_WRITE_ID, payload_len), + MeterResult::Halt + ) { + panic!("Metering halted console write"); + } + let borrowed_memory = memory.as_ref(); + let (fmt_start, fmt_end) = va_range(fmt_ptr as usize, fmt_len as usize); + let fmt_slice = match borrowed_memory.mem_slice(fmt_start, fmt_end) { + Some(s) => s, + None => { + println!("invalid format string @ 0x{:08x}", fmt_ptr); + return 0; + } + }; + let fmt_bytes = fmt_slice.as_ref(); + let fmt = match core::str::from_utf8(fmt_bytes) { + Ok(s) => s, + Err(e) => { + println!("invalid UTF-8 in format string"); + println!("bytes: {:?}", fmt_bytes); + println!("error: {}", e); + return 0; + } + }; + let (args_start, args_end) = va_range(arg_ptr as usize, arg_len as usize); + let args_bytes_slice = borrowed_memory.mem_slice(args_start, args_end); + let args_bytes_holder; + let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { + args_bytes_holder = slice; + args_bytes_holder.as_ref() + } else { + b"" + }; + let raw_args: Vec = args_bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + let mut args: Vec = Vec::new(); + let mut raw_iter = raw_args.into_iter(); + let mut chars = fmt.chars().peekable(); + while let Some(c) = chars.next() { + if c != '%' { + continue; + } + let spec: char = chars.next().unwrap_or('%'); + let mut next = || raw_iter.next().unwrap_or(0); + match spec { + 'd' | 'u' | 'x' => args.push(Arg::U32(next())), + 'f' => args.push(Arg::F32(f32::from_bits(next()))), + 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), + 's' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + let s_ptr = core::str::from_utf8(slice.as_ref()); + args.push(match s_ptr { + Ok(s) => Arg::Str(s.to_string()), + Err(_) => Arg::Str("".to_string()), + }); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'b' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'a' => { + let ptr = next() as usize; + let len = next() as usize; + let byte_len = len * 4; + let (start, end) = va_range(ptr, byte_len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'A' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + _ => args.push(Arg::Str("".to_string())), + } + } + let mut output = String::new(); + let mut args_iter = args.iter(); + let mut fmt_chars = fmt.chars().peekable(); + while let Some(c) = fmt_chars.next() { + if c == '%' { + match fmt_chars.next() { + Some('d') | Some('u') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), + _ => output.push_str(""), + }, + Some('x') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), + _ => output.push_str(""), + }, + Some('f') => match args_iter.next() { + Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), + _ => output.push_str(""), + }, + Some('c') => match args_iter.next() { + Some(Arg::Char(c)) => output.push(*c), + _ => output.push_str(""), + }, + Some('s') => match args_iter.next() { + Some(Arg::Str(s)) => output.push_str(s), + _ => output.push_str(""), + }, + Some('b') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("0x{:02x}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('a') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, chunk) in b.chunks_exact(4).enumerate() { + if i > 0 { + output.push_str(", "); + } + let val = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + output.push_str(&format!("{}", val)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('A') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("{}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('%') => output.push('%'), + Some(_) | None => output.push_str("<%?>"), + } + } else { + output.push(c); + } + } + let _ = caller_mode; + match verbose_writer { + Some(writer) => { + let _ = writeln!(writer.borrow_mut(), "{}", output); + } + None => { + println!("{}", output); + } + } + 0 +} + +fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { + let start = VirtualAddress(ptr as u32); + let end = start.wrapping_add(len as u32); + (start, end) +} diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 98e9241..e0efe5f 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -1,15 +1,42 @@ -use crate::decoder::{decode_full, decode_compressed}; +use crate::decoder::{decode_compressed, decode_full}; use crate::instruction::Instruction; -use crate::memory_page::MemoryPage; -use storage::Storage; -use std::rc::Rc; +use crate::memory::{Memory, VirtualAddress}; +use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use core::cell::RefCell; -use crate::host_interface::HostInterface; -use crate::sys_call::SyscallHandler; -use crate::registers::Register; use core::fmt::Write; use std::collections::HashMap; -use crate::instruction::CsrOp; +use std::rc::Rc; +#[path = "exe.rs"] +mod exec; + +pub const CSR_SATP: u16 = 0x180; +pub const CSR_SSTATUS: u16 = 0x100; +pub const CSR_STVEC: u16 = 0x105; +pub const CSR_SEPC: u16 = 0x141; +pub const CSR_SCAUSE: u16 = 0x142; +pub const CSR_STVAL: u16 = 0x143; +pub const CSR_MEPC: u16 = 0x341; +pub const CSR_MTVEC: u16 = 0x305; +pub const CSR_MCAUSE: u16 = 0x342; +pub const CSR_MTVAL: u16 = 0x343; +const SCAUSE_ECALL_FROM_U: u32 = 8; +const SCAUSE_ECALL_FROM_S: u32 = 9; +const SCAUSE_ECALL_FROM_M: u32 = 11; +const SCAUSE_BREAKPOINT: u32 = 3; +const SSTATUS_SPP: u32 = 1 << 8; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrivilegeMode { + User, + Supervisor, + Machine, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TrapMode { + Supervisor, + Machine, +} /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. /// @@ -36,11 +63,10 @@ use crate::instruction::CsrOp; /// This allows us to run programs written for one architecture (RISC-V) on /// different hardware (like x86 or ARM). The VM provides an abstraction layer /// that makes the underlying hardware details transparent to the running program. -/// -/// MEMORY MANAGEMENT: We use Rc> for shared mutable access to memory -/// and storage, which allows the CPU to read/write memory while maintaining -/// Rust's safety guarantees. -/// +/// +/// MEMORY MANAGEMENT: We use Rc-backed trait objects for shared memory, which +/// allows the CPU to read/write memory while maintaining Rust's safety guarantees. +/// /// PERFORMANCE CONSIDERATIONS: This is an interpretive VM, meaning each /// instruction is decoded and executed one at a time. Real CPUs use techniques /// like pipelining, out-of-order execution, and just-in-time compilation to @@ -62,19 +88,23 @@ pub struct CPU { /// EDUCATIONAL: This helps students understand what the CPU is doing /// by printing each instruction as it executes pub verbose: bool, - pub syscall_handler: Box, - /// Reservation address for LR/SC atomic operations /// EDUCATIONAL: This implements the Load-Reserved/Store-Conditional /// mechanism for atomic memory operations in RISC-V - pub reservation_addr: Option, + pub reservation_addr: Option, /// Optional writer for verbose output /// If None, uses println! to console pub verbose_writer: Option>>, + /// Pluggable metering implementation (gas, resource accounting, etc.) + pub metering: Box, + /// Minimal CSR storage for CSR instructions pub csrs: HashMap, + + /// Current privilege mode (minimal U/S support). + pub priv_mode: PrivilegeMode, } impl std::fmt::Debug for CPU { @@ -84,7 +114,11 @@ impl std::fmt::Debug for CPU { .field("regs", &self.regs) .field("verbose", &self.verbose) .field("reservation_addr", &self.reservation_addr) - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) + .field( + "verbose_writer", + &self.verbose_writer.as_ref().map(|_| "Some()"), + ) + .field("metering", &"") .finish() } } @@ -99,15 +133,23 @@ impl CPU { /// - PC starts at 0 (first instruction) /// - All registers start at 0 (except x0 which is always 0) /// - Verbose logging is disabled by default - pub fn new(syscall_handler: Box) -> Self { + pub fn new() -> Self { + Self::with_metering(Box::new(NoopMeter::default())) + } + + /// Creates a new CPU instance with a custom metering implementation. + pub fn with_metering( + metering: Box, + ) -> Self { Self { pc: 0, regs: [0; 32], verbose: false, - syscall_handler, reservation_addr: None, verbose_writer: None, + metering, csrs: HashMap::new(), + priv_mode: PrivilegeMode::Supervisor, } } @@ -115,6 +157,11 @@ impl CPU { pub fn set_verbose_writer(&mut self, writer: Rc>) { self.verbose_writer = Some(writer); } + + /// Swap in a new metering implementation. + pub fn set_metering(&mut self, metering: Box) { + self.metering = metering; + } /// Helper method to log output /// Only logs if verbose is true and self.verbose is enabled @@ -134,19 +181,135 @@ impl CPU { } } - fn read_csr(&self, csr: u16) -> u32 { + fn can_continue(result: MeterResult) -> bool { + matches!(result, MeterResult::Continue) + } + + fn read_csr(&mut self, csr: u16) -> Option { + if !Self::can_continue(self.metering.on_pc_update(self.pc, self.pc)) { + return None; + } // Provide simple defaults for common CSRs; fall back to stored values or zero. - match csr { + Some(match csr { 0xF14 => *self.csrs.get(&csr).unwrap_or(&0), // mhartid 0xF11 | 0xF12 | 0xF13 => *self.csrs.get(&csr).unwrap_or(&0), // mvendorid/marchid/mimpid 0x301 => *self.csrs.get(&csr).unwrap_or(&0), // misa 0x300 => *self.csrs.get(&csr).unwrap_or(&0), // mstatus + CSR_SSTATUS => *self.csrs.get(&csr).unwrap_or(&0), _ => *self.csrs.get(&csr).unwrap_or(&0), - } + }) } - fn write_csr(&mut self, csr: u16, value: u32) { + fn write_csr(&mut self, csr: u16, value: u32) -> bool { + if !Self::can_continue(self.metering.on_pc_update(self.pc, self.pc)) { + return false; + } self.csrs.insert(csr, value); + true + } + + pub fn set_satp(&mut self, memory: &Memory, value: u32) -> bool { + memory.set_satp(value); + self.write_csr(CSR_SATP, value) + } + + fn set_sstatus_spp(&mut self, prev: PrivilegeMode) { + let mut sstatus = self.read_csr(CSR_SSTATUS).unwrap_or(0); + match prev { + PrivilegeMode::User => sstatus &= !SSTATUS_SPP, + PrivilegeMode::Supervisor => sstatus |= SSTATUS_SPP, + PrivilegeMode::Machine => sstatus |= SSTATUS_SPP, + } + let _ = self.write_csr(CSR_SSTATUS, sstatus); + } + + fn take_sstatus_spp(&mut self) -> PrivilegeMode { + let mut sstatus = self.read_csr(CSR_SSTATUS).unwrap_or(0); + let prev = if sstatus & SSTATUS_SPP != 0 { + PrivilegeMode::Supervisor + } else { + PrivilegeMode::User + }; + sstatus &= !SSTATUS_SPP; + let _ = self.write_csr(CSR_SSTATUS, sstatus); + prev + } + + fn ecall_cause(&self) -> u32 { + match self.priv_mode { + PrivilegeMode::User => SCAUSE_ECALL_FROM_U, + PrivilegeMode::Supervisor => SCAUSE_ECALL_FROM_S, + PrivilegeMode::Machine => SCAUSE_ECALL_FROM_M, + } + } + + fn trap_to_vector( + &mut self, + mode: TrapMode, + cause: u32, + trap_value: u32, + _syscall_id: Option, + ) -> bool { + match mode { + TrapMode::Machine => { + if !self.write_csr(CSR_MEPC, self.pc) { + panic!("trap_to_vector: failed to write mepc"); + } + if !self.write_csr(CSR_MCAUSE, cause) { + panic!("trap_to_vector: failed to write mcause"); + } + if !self.write_csr(CSR_MTVAL, trap_value) { + panic!("trap_to_vector: failed to write mtval"); + } + let mtvec = match self.read_csr(CSR_MTVEC) { + Some(val) => val & !0x3, + None => return false, + }; + self.priv_mode = PrivilegeMode::Machine; + self.set_pc(mtvec) + } + TrapMode::Supervisor => { + if !self.write_csr(CSR_SEPC, self.pc) { + panic!("trap_to_vector: failed to write sepc"); + } + if !self.write_csr(CSR_SCAUSE, cause) { + panic!("trap_to_vector: failed to write scause"); + } + if !self.write_csr(CSR_STVAL, trap_value) { + panic!("trap_to_vector: failed to write stval"); + } + let stvec = match self.read_csr(CSR_STVEC) { + Some(val) => val & !0x3, + None => return false, + }; + self.set_sstatus_spp(self.priv_mode); + self.priv_mode = PrivilegeMode::Supervisor; + self.set_pc(stvec) + } + } + } + + fn has_trap_vector(&self) -> Option { + match self.priv_mode { + PrivilegeMode::Machine => { + if self.csrs.contains_key(&CSR_MTVEC) { + Some(TrapMode::Machine) + } else if self.csrs.contains_key(&CSR_STVEC) { + Some(TrapMode::Supervisor) + } else { + None + } + } + PrivilegeMode::Supervisor | PrivilegeMode::User => { + if self.csrs.contains_key(&CSR_STVEC) { + Some(TrapMode::Supervisor) + } else if self.csrs.contains_key(&CSR_MTVEC) { + Some(TrapMode::Machine) + } else { + None + } + } + } } /// Executes a single instruction cycle (fetch, decode, execute). @@ -171,19 +334,14 @@ impl CPU { /// /// RETURN VALUE: Returns true if execution should continue, false to halt /// - /// MEMORY ACCESS: Uses shared references to memory and storage to allow + /// MEMORY ACCESS: Uses shared references to memory to allow /// the CPU to read/write while maintaining Rust's safety guarantees. /// /// REAL-WORLD ANALOGY: This is like a factory assembly line where each /// worker (instruction) performs a specific task. The conveyor belt (PC) /// moves to the next task automatically, unless a task specifically /// redirects the flow (like a branch or jump instruction). - pub fn step( - &mut self, - memory: Rc>, - storage: Rc>, - host: &mut Box, - ) -> bool { + pub fn step(&mut self, memory: Memory) -> bool { // EDUCATIONAL: Step 1 - Fetch and decode the next instruction let instr = self.next_instruction(Rc::clone(&memory)); @@ -191,11 +349,11 @@ impl CPU { match instr { Some((instr, size)) => { // Valid instruction found - execute it - self.run_instruction(instr, size, Rc::clone(&memory), storage, host) + self.run_instruction(instr, size, Rc::clone(&memory)) } None => { // No valid instruction found - handle the error - self.unknown_instruction(Rc::clone(&memory), storage) + self.unknown_instruction(Rc::clone(&memory)) } } } @@ -213,60 +371,92 @@ impl CPU { /// - instr: The decoded instruction to execute /// - size: Size of the instruction in bytes (2 for compressed, 4 for full) /// - memory: Shared reference to memory for load/store operations - /// - storage: Shared reference to persistent storage fn run_instruction( - &mut self, - instr: Instruction, - size: u8, - memory: Rc>, - storage: Rc>, - host: &mut Box) -> bool { + &mut self, + instr: Instruction, + size: u8, + memory: Memory, + ) -> bool { // EDUCATIONAL: Debug output to help understand what's happening // Get the actual instruction bytes for debugging - if let Some(bytes) = memory.borrow().mem_slice(self.pc as usize, self.pc as usize + size as usize) { - let hex_bytes = bytes.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); - self.log(&format!("PC = 0x{:08x}, Bytes = [{}], Instr = {}", self.pc, hex_bytes, instr.pretty_print()), true); + let pc_va = VirtualAddress(self.pc); + let end_va = VirtualAddress(self.pc.wrapping_add(size as u32)); + if let Some(bytes) = memory.mem_slice(pc_va, end_va) { + let hex_bytes = bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" "); + self.log( + &format!( + "PC = 0x{:08x}, Bytes = [{}], Instr = {}", + self.pc, + hex_bytes, + instr.pretty_print() + ), + true, + ); } else { - self.log(&format!("PC = 0x{:08x}, Instr = {}", self.pc, instr.pretty_print()), true); + self.log( + &format!("PC = 0x{:08x}, Instr = {}", self.pc, instr.pretty_print()), + true, + ); } - + + if !Self::can_continue(self.metering.on_instruction(self.pc, &instr, size)) { + return false; + } + // EDUCATIONAL: Remember the old PC to detect if the instruction changed it let old_pc = self.pc; - + // EDUCATIONAL: Execute the instruction - let result = self.execute(instr, memory, storage, host); + let result = self.execute(instr.clone(), memory); + if !result { + self.log( + &format!( + "Execution halted at PC=0x{:08x} on instr={}", + self.pc, + instr.pretty_print() + ), + false, + ); + } // EDUCATIONAL: Only increment PC if the instruction didn't change it // This handles branches, jumps, and calls correctly if self.pc == old_pc { - self.pc = self.pc.wrapping_add(size as u32); + if !self.pc_add(size as u32) { + return false; + } } result } /// Handles unknown or invalid instructions. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates error handling in CPU design. /// When a CPU encounters an invalid instruction, it needs to handle it /// gracefully rather than crashing. - /// + /// /// DEBUGGING: This function provides detailed information about what /// went wrong, including the hex dump of the invalid bytes. - /// + /// /// RETURN VALUE: Returns false to halt execution on invalid instructions - fn unknown_instruction(&mut self, memory: Rc>, _storage: Rc>) -> bool { + fn unknown_instruction(&mut self, memory: Memory) -> bool { // EDUCATIONAL: Try to read the invalid instruction bytes for debugging - if let Some(slice_ref) = memory.borrow().mem_slice(self.pc as usize, self.pc as usize + 4) { + if let Some(slice_ref) = memory.mem_slice(VirtualAddress(self.pc), VirtualAddress(self.pc.wrapping_add(4))) + { // EDUCATIONAL: Convert bytes to hex for human-readable debugging - let hex_dump = slice_ref.iter() + let hex_dump = slice_ref + .iter() .map(|b| format!("{:02x}", b)) // still needs deref .collect::>() .join(" "); panic!( "🚨 Unknown or invalid instruction at PC = 0x{:08x} (bytes: [{}])", - self.pc, - hex_dump + self.pc, hex_dump ); } else { panic!( @@ -278,22 +468,21 @@ impl CPU { } /// Fetches and decodes the next instruction from memory. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates the fetch and decode phases /// of the instruction cycle. It handles both regular (32-bit) and /// compressed (16-bit) RISC-V instructions. - /// + /// /// RISC-V COMPRESSED INSTRUCTIONS: RISC-V supports 16-bit compressed /// instructions to reduce code size. The bottom 2 bits determine if /// an instruction is compressed (not 0b11) or regular (0b11). - /// + /// /// RETURN VALUE: Returns Some((instruction, size)) if successful, None if invalid - pub fn next_instruction(&mut self, memory: Rc>) -> Option<(Instruction, u8)> { - let pc = self.pc as usize; - let mem_ref = memory.borrow(); - + pub fn next_instruction(&mut self, memory: Memory) -> Option<(Instruction, u8)> { + let pc = VirtualAddress(self.pc); + // EDUCATIONAL: Read 4 bytes from memory (enough for any instruction) - let bytes = mem_ref.mem_slice(pc, pc + 4)?; + let bytes = memory.mem_slice(pc, VirtualAddress(self.pc.wrapping_add(4)))?; // EDUCATIONAL: Need at least 2 bytes for any instruction if bytes.len() < 2 { @@ -317,545 +506,53 @@ impl CPU { } } - /// Safely write to a register, ignoring writes to x0 (which should always be 0) - fn write_reg(&mut self, rd: usize, value: u32) { - if rd != 0 { - self.regs[rd] = value; + /// Safely read a register with metering. + fn read_reg(&mut self, reg: usize) -> Option { + if !Self::can_continue(self.metering.on_register_read(reg)) { + return None; } - // Writes to x0 are ignored (RISC-V specification) + Some(self.regs[reg]) } - - /// Executes a decoded instruction. - /// - /// EDUCATIONAL PURPOSE: This is the execute phase of the instruction cycle. - /// It contains the implementation of all RISC-V instructions supported by - /// our VM. This is where the actual computation happens. - /// - /// INSTRUCTION CATEGORIES: - /// - Arithmetic: ADD, SUB, MUL, DIV, etc. - /// - Logical: AND, OR, XOR, shifts - /// - Memory: Load and store operations - /// - Control: Branches and jumps - /// - System: System calls and special operations - /// - /// REGISTER CONVENTIONS: - /// - rd: Destination register (where result goes) - /// - rs1, rs2: Source registers (operands) - /// - imm: Immediate value (constant) - /// - /// RETURN VALUE: Returns true to continue execution, false to halt - pub fn execute( - &mut self, - instr: Instruction, - memory: Rc>, - storage: Rc>, - host: &mut Box) -> bool { - match instr { - // EDUCATIONAL: Arithmetic instructions - perform mathematical operations - Instruction::Add { rd, rs1, rs2 } => { - // EDUCATIONAL: Use wrapping_add to handle overflow correctly - // In real CPUs, overflow might set flags or cause exceptions - self.write_reg(rd, self.regs[rs1].wrapping_add(self.regs[rs2])) - } - Instruction::Sub { rd, rs1, rs2 } => { - self.write_reg(rd, self.regs[rs1].wrapping_sub(self.regs[rs2])) - } - Instruction::Addi { rd, rs1, imm } => { - self.write_reg(rd, self.regs[rs1].wrapping_add(imm as u32)) - } - - // EDUCATIONAL: Logical instructions - perform bitwise operations - Instruction::And { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] & self.regs[rs2]), - Instruction::Or { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] | self.regs[rs2]), - Instruction::Xor { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] ^ self.regs[rs2]), - Instruction::Andi { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] & (imm as u32)), - Instruction::Ori { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] | (imm as u32)), - Instruction::Xori { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] ^ (imm as u32)), - - // EDUCATIONAL: Comparison instructions - set result to 0 or 1 - Instruction::Slt { rd, rs1, rs2 } => { - // EDUCATIONAL: Set if less than (signed comparison) - self.write_reg(rd, (self.regs[rs1] as i32).lt(&(self.regs[rs2] as i32)) as u32) - } - Instruction::Sltu { rd, rs1, rs2 } => { - // EDUCATIONAL: Set if less than (unsigned comparison) - self.write_reg(rd, (self.regs[rs1].lt(&self.regs[rs2])) as u32) - } - Instruction::Slti { rd, rs1, imm } => { - self.write_reg(rd, (self.regs[rs1] as i32).lt(&imm) as u32) - } - Instruction::Sltiu { rd, rs1, imm } => { - let lhs = self.regs[rs1]; - let rhs = imm as u32; - self.write_reg(rd, if lhs < rhs { 1 } else { 0 }); - } - - // EDUCATIONAL: Shift instructions - move bits left or right - Instruction::Sll { rd, rs1, rs2 } => { - // EDUCATIONAL: Logical left shift - multiply by 2^shift_amount - // The & 0x1F ensures shift amount is 0-31 (5 bits) - self.write_reg(rd, self.regs[rs1] << (self.regs[rs2] & 0x1F)) - } - Instruction::Srl { rd, rs1, rs2 } => { - // EDUCATIONAL: Logical right shift - divide by 2^shift_amount - self.write_reg(rd, self.regs[rs1] >> (self.regs[rs2] & 0x1F)) - } - Instruction::Sra { rd, rs1, rs2 } => { - // EDUCATIONAL: Arithmetic right shift - preserves sign bit - self.write_reg(rd, ((self.regs[rs1] as i32) >> (self.regs[rs2] & 0x1F)) as u32) - } - Instruction::Slli { rd, rs1, shamt } => self.write_reg(rd, self.regs[rs1] << shamt), - Instruction::Srli { rd, rs1, shamt } => self.write_reg(rd, self.regs[rs1] >> shamt), - Instruction::Srai { rd, rs1, shamt } => { - self.write_reg(rd, ((self.regs[rs1] as i32) >> shamt) as u32) - } - - // EDUCATIONAL: Load instructions - read data from memory into registers - Instruction::Lw { rd, rs1, offset } => { - // EDUCATIONAL: Load word (32-bit) from memory - // Address = base register + offset - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - self.write_reg(rd, memory.borrow().load_u32(addr)); - } - Instruction::Ld { rd, rs1, offset } => { - // EDUCATIONAL: Load doubleword (64-bit) from memory, truncated to 32-bit - // Since this is a 32-bit VM, we only load the lower 32 bits - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - self.write_reg(rd, memory.borrow().load_u32(addr)); - } - Instruction::Lb { rd, rs1, offset } => { - // EDUCATIONAL: Load byte (8-bit, sign-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let byte = memory.borrow().load_byte(addr); - let value = (byte as i8) as i32 as u32; // sign-extend to 32-bit - self.write_reg(rd, value); - } - Instruction::Lbu { rd, rs1, offset } => { - // EDUCATIONAL: Load byte unsigned (8-bit, zero-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let byte = memory.borrow().load_byte(addr); - self.write_reg(rd, byte as u32); - } - Instruction::Lh { rd, rs1, offset } => { - // EDUCATIONAL: Load halfword (16-bit, sign-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let halfword = memory.borrow().load_halfword(addr); - let value = (halfword as i16) as i32 as u32; // sign-extend to 32-bit - self.write_reg(rd, value); - } - Instruction::Lhu { rd, rs1, offset } => { - // EDUCATIONAL: Load halfword unsigned (16-bit, zero-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let halfword = memory.borrow().load_halfword(addr); - self.write_reg(rd, halfword as u32); // zero-extend to 32-bit - } - - // EDUCATIONAL: Store instructions - write data from registers to memory - Instruction::Sh { rs1, rs2, offset } => { - // EDUCATIONAL: Store halfword (16-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u16(addr, (self.regs[rs2] & 0xFFFF) as u16); - } - Instruction::Sw { rs1, rs2, offset } => { - // EDUCATIONAL: Store word (32-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u32(addr, self.regs[rs2]); - } - Instruction::Sb { rs1, rs2, offset } => { - // EDUCATIONAL: Store byte (8-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u8(addr, (self.regs[rs2] & 0xFF) as u8); - } - - // EDUCATIONAL: Branch instructions - conditionally change the PC - // These implement if/else and loop constructs - Instruction::Beq { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if equal - jump if two registers are equal - if self.regs[rs1] == self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bne { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if not equal - if self.regs[rs1] != self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Blt { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if less than (signed comparison) - if (self.regs[rs1] as i32) < (self.regs[rs2] as i32) { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bge { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if greater than or equal (signed) - if (self.regs[rs1] as i32) >= (self.regs[rs2] as i32) { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bltu { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if less than (unsigned comparison) - if self.regs[rs1] < self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - - Instruction::Bgeu { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if greater than or equal (unsigned) - if self.regs[rs1] >= self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - // EDUCATIONAL: Jump and Link instructions - for function calls - Instruction::Jal { rd, offset, compressed } => { - // EDUCATIONAL: JAL (Jump and Link) - unconditional jump with return address - // Used for function calls and long-distance jumps - // The return address is stored in rd (usually x1/ra) - let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - self.write_reg(rd, return_address); - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - Instruction::Jalr { rd, rs1, offset , compressed} => { - // EDUCATIONAL: JALR (Jump and Link Register) - indirect function calls - // Target address = base register + offset, with bottom bit cleared - // This ensures proper alignment and is required by RISC-V spec - let base = self.regs[rs1]; - let target = base.wrapping_add(offset as u32) & !1; - - // For compressed instructions (c.jalr), return address should be pc + 2 - // For regular instructions (jalr), return address should be pc + 4 - let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - - self.write_reg(rd, return_address); - - self.pc = target; - return true; - } - - // EDUCATIONAL: Load Upper Immediate - loads immediate into upper bits - Instruction::Lui { rd, imm } => { - // EDUCATIONAL: LUI loads a 20-bit immediate into bits 31-12 of rd - // This is used to load large constants (like addresses) into registers - self.write_reg(rd, (imm << 12) as u32) - } - Instruction::Auipc { rd, imm } => { - // EDUCATIONAL: AUIPC (Add Upper Immediate to PC) - PC-relative addressing - // Used for position-independent code and loading addresses relative to PC - self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)); - } - - // EDUCATIONAL: Multiplication instructions - extended arithmetic - Instruction::Mul { rd, rs1, rs2 } => { - // EDUCATIONAL: MUL - multiply two registers, store lower 32 bits - self.write_reg(rd, self.regs[rs1].wrapping_mul(self.regs[rs2])) - } - Instruction::Mulh { rd, rs1, rs2 } => { - // EDUCATIONAL: MULH - multiply signed, store upper 32 bits - // Properly sign-extend 32-bit values to 64-bit for signed multiplication - let val1 = (self.regs[rs1] as i32) as i64; - let val2 = (self.regs[rs2] as i32) as i64; - let result = val1 * val2; - self.write_reg(rd, (result >> 32) as u32) - } - Instruction::Mulhu { rd, rs1, rs2 } => { - // EDUCATIONAL: MULHU - multiply unsigned, store upper 32 bits - self.write_reg(rd, (((self.regs[rs1] as u64) * (self.regs[rs2] as u64)) >> 32) as u32) - } - Instruction::Mulhsu { rd, rs1, rs2 } => { - // EDUCATIONAL: MULHSU - multiply signed by unsigned, store upper 32 bits - // Properly sign-extend first operand to signed 64-bit, keep second as unsigned 64-bit - let val1 = (self.regs[rs1] as i32) as i64; - let val2 = self.regs[rs2] as u64; - let result = val1 * (val2 as i64); - self.write_reg(rd, (result >> 32) as u32) - } - // EDUCATIONAL: Division and remainder instructions - Instruction::Div { rd, rs1, rs2 } => { - // EDUCATIONAL: DIV - signed division - // RISC-V spec: division by zero returns -1, overflow returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, 0xFFFFFFFF); // -1 in two's complement - } else { - let dividend = self.regs[rs1] as i32; - let divisor = self.regs[rs2] as i32; - - // Check for overflow: -2^31 / -1 = 2^31 (overflow) - if dividend == i32::MIN && divisor == -1 { - self.write_reg(rd, self.regs[rs1]); // Return dividend on overflow - } else { - self.write_reg(rd, (dividend / divisor) as u32) - } - } - } - Instruction::Divu { rd, rs1, rs2 } => { - // EDUCATIONAL: DIVU - unsigned division - // RISC-V spec: division by zero returns 2^XLEN - 1 - if self.regs[rs2] == 0 { - self.write_reg(rd, 0xFFFFFFFF); // 2^32 - 1 - } else { - self.write_reg(rd, self.regs[rs1] / self.regs[rs2]) - } - } - Instruction::Rem { rd, rs1, rs2 } => { - // EDUCATIONAL: REM - signed remainder - // RISC-V spec: remainder by zero returns dividend, overflow returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, self.regs[rs1]) - } else { - let dividend = self.regs[rs1] as i32; - let divisor = self.regs[rs2] as i32; - - // Check for overflow: -2^31 % -1 = 0 (no overflow, but -2^31 % -1 = 0) - if dividend == i32::MIN && divisor == -1 { - self.write_reg(rd, 0) // Remainder of -2^31 % -1 is 0 - } else { - self.write_reg(rd, (dividend % divisor) as u32) - } - } - } - Instruction::Remu { rd, rs1, rs2 } => { - // EDUCATIONAL: REMU - unsigned remainder - // RISC-V spec: remainder by zero returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, self.regs[rs1]) - } else { - self.write_reg(rd, self.regs[rs1] % self.regs[rs2]) - } - } - - // EDUCATIONAL: System instructions - for OS interaction and debugging - Instruction::Ecall => { - // Prepare syscall args from registers - let args = [ - self.regs[Register::A1 as usize], - self.regs[Register::A2 as usize], - self.regs[Register::A3 as usize], - self.regs[Register::A4 as usize], - self.regs[Register::A5 as usize], - self.regs[Register::A6 as usize], - ]; - let call_id = self.regs[Register::A7 as usize]; - let (result, cont) = self.syscall_handler.handle_syscall(call_id, args, memory, storage, host, &mut self.regs); - self.regs[Register::A0 as usize] = result; - return cont; - } - Instruction::Csr { rd, rs1, csr, op, imm } => { - let src = if imm { rs1 as u32 } else { self.regs[rs1] }; - let old = self.read_csr(csr); - - // Apply CSR op semantics - let mut new_val = old; - match op { - CsrOp::Csrrw => { - if !(imm == false && rs1 == 0) { - new_val = src; - } - } - CsrOp::Csrrs => { - if src != 0 { - new_val = old | src; - } - } - CsrOp::Csrrc => { - if src != 0 { - new_val = old & !src; - } - } - } - - if src != 0 || matches!(op, CsrOp::Csrrw) { - self.write_csr(csr, new_val); - } - - if rd != 0 { - self.write_reg(rd, old); - } - } - Instruction::Ebreak => { - // EDUCATIONAL: EBREAK - Environment Break - for debugging - // In real systems, this would trigger a debugger breakpoint - return false - } - Instruction::Mret => { - // Treat MRET as a simple return/halt in this VM + /// Safely write to a register, ignoring writes to x0 (which should always be 0). + /// Returns false if metering halts execution. + fn write_reg(&mut self, rd: usize, value: u32) -> bool { + if rd != 0 { + if !Self::can_continue(self.metering.on_register_write(rd)) { return false; } - - // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions - Instruction::Jr { rs1 } => { - // EDUCATIONAL: JR (Jump Register) - compressed jump to register - self.pc = self.regs[rs1]; - return true; - } - Instruction::Ret => { - // EDUCATIONAL: RET - compressed return instruction - // Equivalent to JR x1 (jump to return address register) - let target = self.regs[1]; // x1 = ra (return address) - if target == 0 || target == 0xFFFF_FFFF { - return false; // halt if ret target is 0 or invalid - } - - self.pc = target; - return true; - } - Instruction::Mv { rd, rs2 } => { - // EDUCATIONAL: MV (Move) - compressed register copy - self.write_reg(rd, self.regs[rs2]) - } - Instruction::Addi16sp { imm } => { - // EDUCATIONAL: ADDI16SP - add immediate to stack pointer - // x2 is the stack pointer (SP) - self.write_reg(2, self.regs[2].wrapping_add(imm as u32)) - } - Instruction::Addi4spn { rd, imm } => { - // EDUCATIONAL: ADDI4SPN - add immediate to SP, store in rd - // Used for stack frame setup in function prologues - self.write_reg(rd, self.regs[2].wrapping_add(imm)); - } - Instruction::Nop => { - // EDUCATIONAL: NOP - No Operation - does nothing - // Used for alignment and timing in real systems - } - Instruction::Beqz { rs1, offset } => { - // EDUCATIONAL: BEQZ - Branch if Equal to Zero (compressed) - if self.regs[rs1] == 0 { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bnez { rs1, offset } => { - // EDUCATIONAL: BNEZ - Branch if Not Equal to Zero (compressed) - if self.regs[rs1] != 0 { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } + self.regs[rd] = value; + } + true + } - // EDUCATIONAL: Miscellaneous ALU operations (compressed) - Instruction::MiscAlu { rd, rs2, op } => { - match op { - crate::instruction::MiscAluOp::Sub => { - // EDUCATIONAL: C.SUB - compressed subtract - self.write_reg(rd, self.regs[rd].wrapping_sub(self.regs[rs2])); - } - crate::instruction::MiscAluOp::Xor => { - // EDUCATIONAL: C.XOR - compressed XOR - self.write_reg(rd, self.regs[rd] ^ self.regs[rs2]); - } - crate::instruction::MiscAluOp::Or => { - // EDUCATIONAL: C.OR - compressed OR - self.write_reg(rd, self.regs[rd] | self.regs[rs2]); - } - crate::instruction::MiscAluOp::And => { - // EDUCATIONAL: C.AND - compressed AND - self.write_reg(rd, self.regs[rd] & self.regs[rs2]); - } - } - } - Instruction::Fence => { - // FENCE is a memory barrier in hardware, but is a no-op in this VM - } - Instruction::Unimp => { - // UNIMP is an unimplemented instruction, treat as a no-op for compatibility - } - // ===== RV32A (Atomics) ===== - Instruction::AmoswapW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - memory.borrow_mut().store_u32(addr, self.regs[rs2]); - self.write_reg(rd, orig); - } - Instruction::AmoaddW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig.wrapping_add(self.regs[rs2]); - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoandW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig & self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoorW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig | self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoxorW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig ^ self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmomaxW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if (orig as i32) > (self.regs[rs2] as i32) { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmominW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if (orig as i32) < (self.regs[rs2] as i32) { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmomaxuW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if orig > self.regs[rs2] { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmominuW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if orig < self.regs[rs2] { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - // ===== RV32A (LR/SC) ===== - Instruction::LrW { rd, rs1 } => { - let addr = self.regs[rs1] as usize; - let value = memory.borrow().load_u32(addr); - self.write_reg(rd, value); - // Set reservation for this address - self.reservation_addr = Some(addr); - } - Instruction::ScW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let value_to_store = self.regs[rs2]; - - // Check if we have a valid reservation for this address - if self.reservation_addr == Some(addr) { - // Reservation is valid, perform the store - memory.borrow_mut().store_u32(addr, value_to_store); - self.write_reg(rd, 0); // 0 = success - // Clear the reservation (it's consumed) - self.reservation_addr = None; - } else { - // No valid reservation, fail - self.write_reg(rd, 1); // 1 = failure - } - } - _ => todo!("unhandled instruction"), + /// Add to the program counter with wrapping semantics and metering. + fn pc_add(&mut self, delta: u32) -> bool { + let old = self.pc; + let new_pc = self.pc.wrapping_add(delta); + if !Self::can_continue(self.metering.on_pc_update(old, new_pc)) { + return false; } + self.pc = new_pc; true + } + + /// Add to the stack pointer (x2) with metering. + fn sp_add(&mut self, delta: u32) -> bool { + let sp = match self.read_reg(2) { + Some(v) => v, + None => return false, + }; + self.write_reg(2, sp.wrapping_add(delta)) + } - } + /// Set the program counter and meter the update. + fn set_pc(&mut self, target: u32) -> bool { + let old = self.pc; + if !Self::can_continue(self.metering.on_pc_update(old, target)) { + return false; + } + self.pc = target; + true + } } diff --git a/crates/vm/src/decoder.rs b/crates/vm/src/decoder.rs index 9c901d9..6d5ecd6 100644 --- a/crates/vm/src/decoder.rs +++ b/crates/vm/src/decoder.rs @@ -412,6 +412,7 @@ pub fn decode_full(word: u32) -> Option { 0 => Some(Instruction::Ecall), 1 => Some(Instruction::Ebreak), 0x302 => Some(Instruction::Mret), + 0x102 => Some(Instruction::Sret), _ => None, } } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs new file mode 100644 index 0000000..2b326d5 --- /dev/null +++ b/crates/vm/src/exe.rs @@ -0,0 +1,1385 @@ +use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_MEPC, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; +use crate::console::{console_write, CONSOLE_WRITE_ID}; +use crate::memory::VirtualAddress; +use crate::instruction::CsrOp; +use crate::registers::Register; + +impl CPU { + /// Executes a decoded instruction. + /// + /// EDUCATIONAL PURPOSE: This is the execute phase of the instruction cycle. + /// It contains the implementation of all RISC-V instructions supported by + /// our VM. This is where the actual computation happens. + /// + /// INSTRUCTION CATEGORIES: + /// - Arithmetic: ADD, SUB, MUL, DIV, etc. + /// - Logical: AND, OR, XOR, shifts + /// - Memory: Load and store operations + /// - Control: Branches and jumps + /// - System: System calls and special operations + /// + /// REGISTER CONVENTIONS: + /// - rd: Destination register (where result goes) + /// - rs1, rs2: Source registers (operands) + /// - imm: Immediate value (constant) + /// + /// RETURN VALUE: Returns true to continue execution, false to halt + pub fn execute( + &mut self, + instr: Instruction, + memory: Memory, + ) -> bool { + match instr { + // EDUCATIONAL: Arithmetic instructions - perform mathematical operations + Instruction::Add { rd, rs1, rs2 } => { + // EDUCATIONAL: Use wrapping_add to handle overflow correctly + // In real CPUs, overflow might set flags or cause exceptions + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_add(rhs)) { + return false; + } + } + Instruction::Sub { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { + return false; + } + } + Instruction::Addi { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_add(imm as u32)) { + return false; + } + } + + // EDUCATIONAL: Logical instructions - perform bitwise operations + Instruction::And { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & rhs) { + return false; + } + } + Instruction::Or { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | rhs) { + return false; + } + } + Instruction::Xor { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ rhs) { + return false; + } + } + Instruction::Andi { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & (imm as u32)) { + return false; + } + } + Instruction::Ori { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | (imm as u32)) { + return false; + } + } + Instruction::Xori { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ (imm as u32)) { + return false; + } + } + + // EDUCATIONAL: Comparison instructions - set result to 0 or 1 + Instruction::Slt { rd, rs1, rs2 } => { + // EDUCATIONAL: Set if less than (signed comparison) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs as i32).lt(&(rhs as i32)) as u32) { + return false; + } + } + Instruction::Sltu { rd, rs1, rs2 } => { + // EDUCATIONAL: Set if less than (unsigned comparison) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs < rhs) as u32) { + return false; + } + } + Instruction::Slti { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs as i32).lt(&imm) as u32) { + return false; + } + } + Instruction::Sltiu { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = imm as u32; + if !self.write_reg(rd, if lhs < rhs { 1 } else { 0 }) { + return false; + } + } + + // EDUCATIONAL: Shift instructions - move bits left or right + Instruction::Sll { rd, rs1, rs2 } => { + // EDUCATIONAL: Logical left shift - multiply by 2^shift_amount + // The & 0x1F ensures shift amount is 0-31 (5 bits) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs << (rhs & 0x1F)) { + return false; + } + } + Instruction::Srl { rd, rs1, rs2 } => { + // EDUCATIONAL: Logical right shift - divide by 2^shift_amount + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs >> (rhs & 0x1F)) { + return false; + } + } + Instruction::Sra { rd, rs1, rs2 } => { + // EDUCATIONAL: Arithmetic right shift - preserves sign bit + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, ((lhs as i32) >> (rhs & 0x1F)) as u32) { + return false; + } + } + Instruction::Slli { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs << shamt) { + return false; + } + } + Instruction::Srli { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs >> shamt) { + return false; + } + } + Instruction::Srai { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, ((lhs as i32) >> shamt) as u32) { + return false; + } + } + + // EDUCATIONAL: Load instructions - read data from memory into registers + Instruction::Lw { rd, rs1, offset } => { + // EDUCATIONAL: Load word (32-bit) from memory + // Address = base register + offset + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let val = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { + return false; + } + } + Instruction::Ld { rd, rs1, offset } => { + // EDUCATIONAL: Load doubleword (64-bit) from memory, truncated to 32-bit + // Since this is a 32-bit VM, we only load the lower 32 bits + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let val = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { + return false; + } + } + Instruction::Lb { rd, rs1, offset } => { + // EDUCATIONAL: Load byte (8-bit, sign-extended) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let byte = + match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + let value = (byte as i8) as i32 as u32; // sign-extend to 32-bit + if !self.write_reg(rd, value) { + return false; + } + } + Instruction::Lbu { rd, rs1, offset } => { + // EDUCATIONAL: Load byte unsigned (8-bit, zero-extended) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let byte = + match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, byte as u32) { + return false; + } + } + Instruction::Lh { rd, rs1, offset } => { + // EDUCATIONAL: Load halfword (16-bit, sign-extended) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let halfword = match memory.load_halfword( + addr, + self.metering.as_mut(), + MemoryAccessKind::Load, + ) { + Some(v) => v, + None => return false, + }; + let value = (halfword as i16) as i32 as u32; // sign-extend to 32-bit + if !self.write_reg(rd, value) { + return false; + } + } + Instruction::Lhu { rd, rs1, offset } => { + // EDUCATIONAL: Load halfword unsigned (16-bit, zero-extended) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let halfword = match memory.load_halfword( + addr, + self.metering.as_mut(), + MemoryAccessKind::Load, + ) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, halfword as u32) { + return false; + } // zero-extend to 32-bit + } + + // EDUCATIONAL: Store instructions - write data from registers to memory + Instruction::Sh { rs1, rs2, offset } => { + // EDUCATIONAL: Store halfword (16-bit) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u16( + addr, + (src & 0xFFFF) as u16, + self.metering.as_mut(), + MemoryAccessKind::Store, + ) { + return false; + } + } + Instruction::Sw { rs1, rs2, offset } => { + // EDUCATIONAL: Store word (32-bit) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Store) { + return false; + } + } + Instruction::Sb { rs1, rs2, offset } => { + // EDUCATIONAL: Store byte (8-bit) + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u8( + addr, + (src & 0xFF) as u8, + self.metering.as_mut(), + MemoryAccessKind::Store, + ) { + return false; + } + } + + // EDUCATIONAL: Branch instructions - conditionally change the PC + // These implement if/else and loop constructs + Instruction::Beq { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if equal - jump if two registers are equal + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if lhs == rhs { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + Instruction::Bne { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if not equal + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if lhs != rhs { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + Instruction::Blt { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if less than (signed comparison) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if (lhs as i32) < (rhs as i32) { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + Instruction::Bge { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if greater than or equal (signed) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if (lhs as i32) >= (rhs as i32) { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + Instruction::Bltu { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if less than (unsigned comparison) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if lhs < rhs { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + + Instruction::Bgeu { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if greater than or equal (unsigned) + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if lhs >= rhs { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + // EDUCATIONAL: Jump and Link instructions - for function calls + Instruction::Jal { + rd, + offset, + compressed, + } => { + // EDUCATIONAL: JAL (Jump and Link) - unconditional jump with return address + // Used for function calls and long-distance jumps + // The return address is stored in rd (usually x1/ra) + let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; + if !self.write_reg(rd, return_address) { + return false; + } + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + Instruction::Jalr { + rd, + rs1, + offset, + compressed, + } => { + // EDUCATIONAL: JALR (Jump and Link Register) - indirect function calls + // Target address = base register + offset, with bottom bit cleared + // This ensures proper alignment and is required by RISC-V spec + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let target = base.wrapping_add(offset as u32) & !1; + + // For compressed instructions (c.jalr), return address should be pc + 2 + // For regular instructions (jalr), return address should be pc + 4 + let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; + + if !self.write_reg(rd, return_address) { + return false; + } + + if !self.set_pc(target) { + return false; + } + return true; + } + + // EDUCATIONAL: Load Upper Immediate - loads immediate into upper bits + Instruction::Lui { rd, imm } => { + // EDUCATIONAL: LUI loads a 20-bit immediate into bits 31-12 of rd + // This is used to load large constants (like addresses) into registers + if !self.write_reg(rd, (imm << 12) as u32) { + return false; + } + } + Instruction::Auipc { rd, imm } => { + // EDUCATIONAL: AUIPC (Add Upper Immediate to PC) - PC-relative addressing + // Used for position-independent code and loading addresses relative to PC + if !self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)) { + return false; + } + } + + // EDUCATIONAL: Multiplication instructions - extended arithmetic + Instruction::Mul { rd, rs1, rs2 } => { + // EDUCATIONAL: MUL - multiply two registers, store lower 32 bits + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_mul(rhs)) { + return false; + } + } + Instruction::Mulh { rd, rs1, rs2 } => { + // EDUCATIONAL: MULH - multiply signed, store upper 32 bits + // Properly sign-extend 32-bit values to 64-bit for signed multiplication + let val1 = (match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } as i32) as i64; + let val2 = (match self.read_reg(rs2) { + Some(v) => v, + None => return false, + } as i32) as i64; + let result = val1 * val2; + if !self.write_reg(rd, (result >> 32) as u32) { + return false; + } + } + Instruction::Mulhu { rd, rs1, rs2 } => { + // EDUCATIONAL: MULHU - multiply unsigned, store upper 32 bits + let lhs = match self.read_reg(rs1) { + Some(v) => v as u64, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v as u64, + None => return false, + }; + if !self.write_reg(rd, ((lhs * rhs) >> 32) as u32) { + return false; + } + } + Instruction::Mulhsu { rd, rs1, rs2 } => { + // EDUCATIONAL: MULHSU - multiply signed by unsigned, store upper 32 bits + // Properly sign-extend first operand to signed 64-bit, keep second as unsigned 64-bit + let val1 = (match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } as i32) as i64; + let val2 = match self.read_reg(rs2) { + Some(v) => v as u64, + None => return false, + }; + let result = val1 * (val2 as i64); + if !self.write_reg(rd, (result >> 32) as u32) { + return false; + } + } + // EDUCATIONAL: Division and remainder instructions + Instruction::Div { rd, rs1, rs2 } => { + // EDUCATIONAL: DIV - signed division + // RISC-V spec: division by zero returns -1, overflow returns dividend + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if rhs == 0 { + if !self.write_reg(rd, 0xFFFFFFFF) { + return false; + } // -1 in two's complement + } else { + let dividend = lhs as i32; + let divisor = rhs as i32; + + // Check for overflow: -2^31 / -1 = 2^31 (overflow) + if dividend == i32::MIN && divisor == -1 { + if !self.write_reg(rd, lhs) { + return false; + } // Return dividend on overflow + } else { + if !self.write_reg(rd, (dividend / divisor) as u32) { + return false; + } + } + } + } + Instruction::Divu { rd, rs1, rs2 } => { + // EDUCATIONAL: DIVU - unsigned division + // RISC-V spec: division by zero returns 2^XLEN - 1 + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if rhs == 0 { + if !self.write_reg(rd, 0xFFFFFFFF) { + return false; + } // 2^32 - 1 + } else { + if !self.write_reg(rd, lhs / rhs) { + return false; + } + } + } + Instruction::Rem { rd, rs1, rs2 } => { + // EDUCATIONAL: REM - signed remainder + // RISC-V spec: remainder by zero returns dividend, overflow returns dividend + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if rhs == 0 { + if !self.write_reg(rd, lhs) { + return false; + } + } else { + let dividend = lhs as i32; + let divisor = rhs as i32; + + // Check for overflow: -2^31 % -1 = 0 (no overflow, but -2^31 % -1 = 0) + if dividend == i32::MIN && divisor == -1 { + if !self.write_reg(rd, 0) { + return false; + } // Remainder of -2^31 % -1 is 0 + } else { + if !self.write_reg(rd, (dividend % divisor) as u32) { + return false; + } + } + } + } + Instruction::Remu { rd, rs1, rs2 } => { + // EDUCATIONAL: REMU - unsigned remainder + // RISC-V spec: remainder by zero returns dividend + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if rhs == 0 { + if !self.write_reg(rd, lhs) { + return false; + } + } else { + if !self.write_reg(rd, lhs % rhs) { + return false; + } + } + } + + // EDUCATIONAL: System instructions - for OS interaction and debugging + Instruction::Ecall => { + // Prepare syscall args from registers + let args = [ + match self.read_reg(Register::A1 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A2 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A3 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A4 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A5 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A6 as usize) { + Some(v) => v, + None => return false, + }, + ]; + let call_id = match self.read_reg(Register::A7 as usize) { + Some(v) => v, + None => return false, + }; + if call_id == CONSOLE_WRITE_ID { + let result = console_write( + args, + self.priv_mode, + memory, + self.metering.as_mut(), + &self.verbose_writer, + ); + if !self.write_reg(Register::A0 as usize, result) { + return false; + } + return true; + } + if let Some(trap_mode) = self.has_trap_vector() { + if !self.trap_to_vector(trap_mode, self.ecall_cause(), 0, Some(call_id)) { + panic!( + "trap_to_vector returned false for ecall id={} pc=0x{:08x}", + call_id, self.pc + ); + } + return true; + } + panic!( + "ecall without trap vector for id={} pc=0x{:08x}", + call_id, self.pc + ); + } + Instruction::Csr { + rd, + rs1, + csr, + op, + imm, + } => { + let src = if imm { + rs1 as u32 + } else { + match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } + }; + let old = match self.read_csr(csr) { + Some(v) => v, + None => return false, + }; + + // Apply CSR op semantics + let mut new_val = old; + match op { + CsrOp::Csrrw => { + if !(imm == false && rs1 == 0) { + new_val = src; + } + } + CsrOp::Csrrs => { + if src != 0 { + new_val = old | src; + } + } + CsrOp::Csrrc => { + if src != 0 { + new_val = old & !src; + } + } + } + + let will_write = src != 0 || matches!(op, CsrOp::Csrrw); + if will_write { + if csr == CSR_SATP { + if !self.set_satp(&memory, new_val) { + panic!("failed to update satp"); + } + } else if !self.write_csr(csr, new_val) { + panic!("failed to write csr 0x{:03x}", csr); + } + } + + if rd != 0 && !self.write_reg(rd, old) { + return false; + } + } + Instruction::Ebreak => { + // EDUCATIONAL: EBREAK - Environment Break - for debugging + // In real systems, this would trigger a debugger breakpoint + if self.priv_mode == super::PrivilegeMode::User { + if let Some(trap_mode) = self.has_trap_vector() { + if !self.trap_to_vector(trap_mode, SCAUSE_BREAKPOINT, 0, None) { + panic!( + "trap_to_vector returned false for ebreak pc=0x{:08x}", + self.pc + ); + } + return true; + } + } + return false; + } + Instruction::Mret => { + let target = match self.read_csr(CSR_MEPC).or_else(|| self.read_csr(CSR_SEPC)) { + Some(v) => v, + None => return false, + }; + let prev = self.take_sstatus_spp(); + if !self.set_pc(target) { + return false; + } + self.priv_mode = prev; + return true; + } + Instruction::Sret => { + let target = match self.read_csr(CSR_SEPC) { + Some(v) => v, + None => return false, + }; + let prev = self.take_sstatus_spp(); + if !self.set_pc(target) { + return false; + } + self.priv_mode = prev; + return true; + } + + // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions + Instruction::Jr { rs1 } => { + // EDUCATIONAL: JR (Jump Register) - compressed jump to register + let target = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.set_pc(target) { + return false; + } + return true; + } + Instruction::Ret => { + // EDUCATIONAL: RET - compressed return instruction + // Equivalent to JR x1 (jump to return address register) + let target = match self.read_reg(1) { + Some(v) => v, + None => return false, + }; // x1 = ra (return address) + if target == 0 || target == 0xFFFF_FFFF { + return false; // halt if ret target is 0 or invalid + } + + if !self.set_pc(target) { + return false; + } + return true; + } + Instruction::Mv { rd, rs2 } => { + // EDUCATIONAL: MV (Move) - compressed register copy + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, src) { + return false; + } + } + Instruction::Addi16sp { imm } => { + // EDUCATIONAL: ADDI16SP - add immediate to stack pointer + // x2 is the stack pointer (SP) + if !self.sp_add(imm as u32) { + return false; + } + } + Instruction::Addi4spn { rd, imm } => { + // EDUCATIONAL: ADDI4SPN - add immediate to SP, store in rd + // Used for stack frame setup in function prologues + let sp = match self.read_reg(2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, sp.wrapping_add(imm)) { + return false; + } + } + Instruction::Nop => { + // EDUCATIONAL: NOP - No Operation - does nothing + // Used for alignment and timing in real systems + } + Instruction::Beqz { rs1, offset } => { + // EDUCATIONAL: BEQZ - Branch if Equal to Zero (compressed) + let val = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if val == 0 { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + Instruction::Bnez { rs1, offset } => { + // EDUCATIONAL: BNEZ - Branch if Not Equal to Zero (compressed) + let val = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if val != 0 { + if !self.pc_add(offset as u32) { + return false; + } + return true; + } + } + + // EDUCATIONAL: Miscellaneous ALU operations (compressed) + Instruction::MiscAlu { rd, rs2, op } => { + match op { + crate::instruction::MiscAluOp::Sub => { + // EDUCATIONAL: C.SUB - compressed subtract + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { + return false; + } + } + crate::instruction::MiscAluOp::Xor => { + // EDUCATIONAL: C.XOR - compressed XOR + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ rhs) { + return false; + } + } + crate::instruction::MiscAluOp::Or => { + // EDUCATIONAL: C.OR - compressed OR + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | rhs) { + return false; + } + } + crate::instruction::MiscAluOp::And => { + // EDUCATIONAL: C.AND - compressed AND + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & rhs) { + return false; + } + } + } + } + Instruction::Fence => { + // FENCE is a memory barrier in hardware, but is a no-op in this VM + } + Instruction::Unimp => { + // UNIMP is an unimplemented instruction, treat as a no-op for compatibility + } + // ===== RV32A (Atomics) ===== + Instruction::AmoswapW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + if !memory.store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmoaddW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig.wrapping_add(src); + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmoandW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig & src; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmoorW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig | src; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmoxorW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig ^ src; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmomaxW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) > (src as i32) { + orig + } else { + src + }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmominW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) < (src as i32) { + orig + } else { + src + }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmomaxuW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if orig > src { orig } else { src }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + Instruction::AmominuW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if orig < src { orig } else { src }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { + return false; + } + } + // ===== RV32A (LR/SC) ===== + Instruction::LrW { rd, rs1 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let value = match memory.load_u32( + addr, + self.metering.as_mut(), + MemoryAccessKind::ReservationLoad, + ) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, value) { + return false; + } + // Set reservation for this address + self.reservation_addr = Some(addr); + } + Instruction::ScW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let addr = VirtualAddress(base); + let value_to_store = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + + // Check if we have a valid reservation for this address + if self.reservation_addr == Some(addr) { + // Reservation is valid, perform the store + if !memory.store_u32( + addr, + value_to_store, + self.metering.as_mut(), + MemoryAccessKind::ReservationStore, + ) { + return false; + } + if !self.write_reg(rd, 0) { + return false; + } // 0 = success + // Clear the reservation (it's consumed) + self.reservation_addr = None; + } else { + // No valid reservation, fail + if !self.write_reg(rd, 1) { + return false; + } // 1 = failure + } + } + _ => todo!("unhandled instruction"), + } + true + } +} diff --git a/crates/vm/src/host_interface.rs b/crates/vm/src/host_interface.rs deleted file mode 100644 index 82c48ec..0000000 --- a/crates/vm/src/host_interface.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt::Debug; - -pub trait HostInterface: Debug { - // calls another program, returns result ptr and page index - fn call_program(&mut self, from: [u8; 20], to: [u8; 20], input_data: Vec) -> (u32, usize); - fn read_memory_page(&mut self, page_index: usize, guest_ptr: u32, len: usize) -> Option>; - fn fire_event(&mut self, event: Vec); - fn transfer(&mut self, to: [u8; 20], value: u64) -> bool; - fn balance(&mut self, addr: [u8; 20]) -> u128; -} - -#[derive(Debug)] -pub struct NoopHost; - -impl HostInterface for NoopHost { - fn call_program(&mut self, _from: [u8; 20], _to: [u8; 20], _input_data: Vec) -> (u32, usize) { - (0, 0) - } - fn read_memory_page(&mut self, _page_index: usize, _guest_ptr: u32, _len: usize) -> Option> { - None - } - fn fire_event(&mut self, _event: Vec) { - // No operation - } - fn transfer(&mut self, _to: [u8; 20], _value: u64) -> bool { - false - } - fn balance(&mut self, _addr: [u8; 20]) -> u128 { - 0 - } -} diff --git a/crates/vm/src/instruction.rs b/crates/vm/src/instruction.rs index fcfe3cc..fc28c0a 100644 --- a/crates/vm/src/instruction.rs +++ b/crates/vm/src/instruction.rs @@ -34,7 +34,7 @@ /// PERFORMANCE IMPLICATIONS: Different instruction types have different /// execution costs. Memory operations are typically slower than register /// operations, and branches can cause pipeline stalls in real CPUs. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Instruction { // ===== RV32I ===== @@ -446,6 +446,8 @@ pub enum Instruction { /// MRET: Machine-mode return (treated as a halt in this VM) Mret, + /// SRET: Supervisor-mode return + Sret, /// C.MISC-ALU: compressed miscellaneous ALU operations /// EDUCATIONAL: Compressed miscellaneous ALU operations including C.SUB, C.XOR, C.OR, C.AND. @@ -486,7 +488,7 @@ pub enum CsrOp { /// EDUCATIONAL: Miscellaneous ALU operations for compressed instructions. /// These represent the different operations that can be performed by the C.MISC-ALU instruction. /// Each operation is a 16-bit compressed version of a corresponding 32-bit instruction. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum MiscAluOp { /// C.SUB: rd = rd - rs2 (compressed subtract) /// EDUCATIONAL: Compressed subtract operation. Subtracts rs2 from rd and stores result in rd. @@ -670,6 +672,8 @@ impl Instruction { "ebreak".to_string(), Instruction::Mret => "mret".to_string(), + Instruction::Sret => + "sret".to_string(), Instruction::Csr { rd, rs1, csr, op, imm } => { let op_str = match op { CsrOp::Csrrw => if *imm { "csrrwi" } else { "csrrw" }, diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index ffc1dfe..0df3e06 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -1,10 +1,10 @@ +pub mod cpu; +pub mod console; +pub mod decoder; +pub mod instruction; pub mod isa; pub mod isa_compressed; -pub mod instruction; -pub mod decoder; -pub mod vm; -pub mod cpu; +pub mod memory; +pub mod metering; pub mod registers; -pub mod memory_page; -pub mod sys_call; -pub mod host_interface; \ No newline at end of file +pub mod vm; diff --git a/crates/vm/src/memory/mod.rs b/crates/vm/src/memory/mod.rs new file mode 100644 index 0000000..1cab39b --- /dev/null +++ b/crates/vm/src/memory/mod.rs @@ -0,0 +1,133 @@ +use std::cell::Ref; +use std::rc::Rc; + +use crate::metering::{MemoryAccessKind, Metering}; + +mod sv32; + +pub use sv32::Sv32Memory; +pub use types::mmu::*; + +pub const HEAP_PTR_OFFSET: u32 = 0x100; + +pub const PAGE_SIZE: usize = 4096; +pub const PAGE_SHIFT: u32 = 12; +pub const VPN_MASK: u32 = 0x3ff; +pub const PAGE_OFFSET_MASK: u32 = 0xfff; + +/// Simple permission bits for page mappings (mirrors Sv32 R/W/X/U). +#[derive(Clone, Copy, Debug)] +pub struct Perms { + pub read: bool, + pub write: bool, + pub exec: bool, + pub user: bool, +} + +impl Perms { + pub const fn new(read: bool, write: bool, exec: bool, user: bool) -> Self { + Self { + read, + write, + exec, + user, + } + } + + pub fn rwx_kernel() -> Self { + Self::new(true, true, true, false) + } + + pub fn rw_kernel() -> Self { + Self::new(true, true, false, false) + } +} + +/// Sv32 virtual address helper newtype. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VirtualAddress(pub u32); + +impl VirtualAddress { + pub fn as_u32(self) -> u32 { + self.0 + } + + pub fn as_usize(self) -> usize { + self.0 as usize + } + + pub fn offset(self) -> u32 { + self.0 & PAGE_OFFSET_MASK + } + + pub fn vpn0(self) -> u32 { + (self.0 >> PAGE_SHIFT) & VPN_MASK + } + + pub fn vpn1(self) -> u32 { + (self.0 >> (PAGE_SHIFT + 10)) & VPN_MASK + } + + pub fn align_down(self) -> Self { + VirtualAddress(self.0 & !(PAGE_OFFSET_MASK)) + } + + pub fn wrapping_add(self, value: u32) -> Self { + VirtualAddress(self.0.wrapping_add(value)) + } + + pub fn checked_add(self, value: u32) -> Option { + self.0.checked_add(value).map(VirtualAddress) + } +} + +impl From for VirtualAddress { + fn from(value: u32) -> Self { + VirtualAddress(value) + } +} + +impl From for VirtualAddress { + fn from(value: usize) -> Self { + VirtualAddress(value as u32) + } +} + +impl From for usize { + fn from(value: VirtualAddress) -> Self { + value.as_usize() + } +} + +pub trait MMU: std::fmt::Debug { + // --- CPU-facing data access (loads/stores/fetches) --- + fn mem(&self) -> Ref>; + fn mem_slice(&self, start: VirtualAddress, end: VirtualAddress) -> Option>; + fn store_u16(&self, addr: VirtualAddress, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u32(&self, addr: VirtualAddress, val: u32, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u8(&self, addr: VirtualAddress, val: u8, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn load_u32(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; +} + +pub trait API: std::fmt::Debug { + fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms); + /// Get the current page-table root (index/identifier). + fn current_root(&self) -> usize; + /// Read the current satp value. + fn satp(&self) -> u32; + /// Set satp (PPN field is used for the root in this emulator). + fn set_satp(&self, satp: u32); + /// Top of the stack for this memory layout. + fn stack_top(&self) -> VirtualAddress; + fn size(&self) -> usize; + fn offset(&self, addr: VirtualAddress) -> usize; +} + +pub trait Mmu: MMU + API {} + +impl Mmu for T {} + +pub type Memory = Rc; diff --git a/crates/vm/src/memory/sv32.rs b/crates/vm/src/memory/sv32.rs new file mode 100644 index 0000000..3d46219 --- /dev/null +++ b/crates/vm/src/memory/sv32.rs @@ -0,0 +1,459 @@ +use std::cell::{Cell, Ref, RefCell}; +use std::rc::Rc; + +use crate::metering::{MemoryAccessKind, MeterResult, Metering}; + +use types::{ + map_allocating, map_to_physical, Sv32PagePerms, Sv32PageTable, SV32_PTE_R, SV32_PTE_V, + SV32_PTE_W, SV32_PTE_X, SV32_SATP_PPN_MASK, +}; + +use super::{API, MMU, Perms, VirtualAddress}; + +/// Software Sv32 MMU backed by a contiguous physical buffer. +/// +/// Design at a glance: +/// - Physical memory is a single `Vec` (`backing`). Frames are 4 KiB slices into it. +/// - Virtualβ†’physical is resolved with Sv32-style page tables: L1 root (VPN1) and L2 (VPN0). +/// - Page tables live in guest memory; `translate` walks them using the satp root PPN. +/// - A bump frame allocator hands out PPNs (physical page numbers) sequentially from the backing; no free list yet. +/// - Mapping APIs (`map_page`/`map_range`) allocate tables/frames and set R/W/X/U bits. +/// - `translate` walks VPN1β†’VPN0, checks permissions against the access kind, and returns a byte +/// offset into the backing. All loads/stores go through this path. +/// - Heap management is handled outside the MMU. +/// +/// Limitations/assumptions: +/// - No unmap or reuse of frames yet; the allocator only grows. +/// - No access/dirty bits; permissions are R/W/X/U/V only. +/// - `mem_slice` only returns contiguous slices when the mapped physical pages are contiguous. +/// - Identity mapping is not assumed; everything uses page tables even for kernel. +#[derive(Debug)] +pub struct Sv32Memory { + /// Page size in bytes (Sv32: 4 KiB). + page_size: usize, + /// Total number of physical frames available. + total_pages: usize, + /// Contiguous physical backing store. + backing: Rc>>, + /// satp value that selects the active root PPN. + satp: Cell, + /// Next free physical frame index for frame allocation. + next_free_frame: Cell, +} + +fn perms_to_sv32(perms: Perms) -> Sv32PagePerms { + Sv32PagePerms { + read: perms.read, + write: perms.write, + exec: perms.exec, + user: perms.user, + } +} + +impl Sv32Memory { + pub fn new(total_size_bytes: usize, page_size: usize) -> Self { + assert!(page_size != 0, "page_size must be > 0"); + assert!(total_size_bytes != 0, "total_size_bytes must be > 0"); + + let total_pages = (total_size_bytes + (page_size - 1)) / page_size; + let total = total_pages + .checked_mul(page_size) + .expect("physical memory size overflow"); + // Reserve frame 0; place the initial root page table at frame 1. + let root_ppn: usize = 1; + let mem = Self { + page_size, + total_pages, + backing: Rc::new(RefCell::new(vec![0u8; total])), + satp: Cell::new(root_ppn as u32), + next_free_frame: Cell::new(root_ppn + 1), + }; + // Zero the root page table frame so we can immediately populate it. + mem.zero_frame(root_ppn); + mem + } + + fn total_size(&self) -> usize { + self.backing.borrow().len() + } + + fn root_ppn(&self) -> usize { + (self.satp.get() & SV32_SATP_PPN_MASK) as usize + } + + fn root_base(&self) -> Option { + let base = self.root_ppn().checked_mul(self.page_size)?; + if base + self.page_size > self.total_size() { + None + } else { + Some(base) + } + } + + /// Allocate a physical frame (4 KiB) and return its page number, or None if out of frames. + fn allocate_frame(&self) -> Option { + let frame = self.next_free_frame.get(); + if frame >= self.total_pages { + return None; + } + self.next_free_frame.set(frame + 1); + Some(frame) + } + + pub fn next_free_ppn(&self) -> usize { + self.next_free_frame.get() + } + + fn zero_frame(&self, ppn: usize) { + let mut backing = self.backing.borrow_mut(); + let start = ppn + .checked_mul(self.page_size) + .expect("frame offset overflow"); + let end = start + self.page_size; + backing[start..end].fill(0); + } + + fn read_pte(&self, phys_addr: usize) -> Option { + let backing = self.backing.borrow(); + let end = phys_addr.checked_add(4)?; + if end > backing.len() { + return None; + } + Some(u32::from_le_bytes( + backing[phys_addr..end].try_into().unwrap(), + )) + } + + fn write_pte(&self, phys_addr: usize, val: u32) { + let mut backing = self.backing.borrow_mut(); + let end = phys_addr + .checked_add(4) + .expect("pte write offset overflow"); + if end > backing.len() { + panic!("pte write out of bounds"); + } + backing[phys_addr..end].copy_from_slice(&val.to_le_bytes()); + } + + /// Map a contiguous virtual range page-by-page with the given permissions. + pub fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { + let root = self.root_ppn() as u32; + let ok = map_allocating(self, root, start.as_u32(), len, perms_to_sv32(perms)); + assert!(ok, "map_range failed"); + } + + /// Map a virtual range to a specific physical range without allocating new leaf frames. + pub fn map_physical_range( + &self, + va_start: VirtualAddress, + phys_start: u32, + len: usize, + perms: Perms, + ) -> bool { + map_to_physical( + self, + self.root_ppn() as u32, + va_start.as_u32(), + phys_start, + len, + perms_to_sv32(perms), + ) + } + + /// Translate a virtual address to a physical offset into `backing`, checking permissions. + /// + /// This emulates an Sv32 page-table walk driven by the current `satp`: + /// - `satp` PPN selects the root L1 page table (written by the kernel in guest memory). + /// - We read the L1 PTE at VPN1; it must be valid and non-leaf (no superpages here). + /// - From that PPN we read the L2 PTE at VPN0; it must be valid and carry R/W/X bits. + /// - Permissions are checked against the access kind; on success we return a byte offset + /// into the physical backing buffer. + /// + /// All PTE bytes we read here are what the kernel previously wrote into guest memory; + /// the host MMU just interprets them to enforce translations. + fn translate(&self, va: VirtualAddress, kind: MemoryAccessKind) -> Option { + let root_base = self.root_base()?; + let vpn1 = va.vpn1() as usize; + let vpn0 = va.vpn0() as usize; + let offset = va.offset() as usize; + + let root_pte_addr = root_base + vpn1 * core::mem::size_of::(); + let root_pte = self.read_pte(root_pte_addr)?; + if root_pte & SV32_PTE_V == 0 { + return None; + } + + // We only support two-level translation; reject L1 leaf/superpages. + if root_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return None; + } + + let l2_ppn = (root_pte >> 10) as usize; + let l2_base = l2_ppn + .checked_mul(self.page_size) + .expect("l2 base overflow"); + let l2_pte_addr = l2_base + vpn0 * core::mem::size_of::(); + let l2_pte = self.read_pte(l2_pte_addr)?; + if l2_pte & SV32_PTE_V == 0 { + return None; + } + + let allowed = match kind { + MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => { + l2_pte & (SV32_PTE_R | SV32_PTE_X) != 0 + } + MemoryAccessKind::Store | MemoryAccessKind::Atomic | MemoryAccessKind::ReservationStore => { + l2_pte & SV32_PTE_W != 0 + } + }; + if !allowed { + return None; + } + + let leaf_ppn = (l2_pte >> 10) as usize; + leaf_ppn + .checked_mul(self.page_size) + .and_then(|base| base.checked_add(offset)) + } + + fn meter_access( + metering: &mut dyn Metering, + kind: MemoryAccessKind, + addr: VirtualAddress, + bytes: usize, + ) -> bool { + matches!( + metering.on_memory_access(kind, addr.as_usize(), bytes), + MeterResult::Continue + ) + } + + /// Copy a slice into physical backing, honoring translation and page boundaries. + fn copy_into_backing(&self, start: VirtualAddress, data: &[u8], kind: MemoryAccessKind) { + let mut remaining = data.len(); + let mut offset_in_data = 0usize; + let mut va = start; + while remaining > 0 { + let phys = self + .translate(va, kind) + .expect("copy failed: unmapped virtual address"); + let page_remaining = self.page_size - (va.offset() as usize); + let to_copy = core::cmp::min(page_remaining, remaining); + { + let mut backing = self.backing.borrow_mut(); + let dst = phys; + let src_start = offset_in_data; + let src_end = src_start + to_copy; + backing[dst..dst + to_copy].copy_from_slice(&data[src_start..src_end]); + } + remaining -= to_copy; + offset_in_data += to_copy; + va = VirtualAddress(va.as_u32().wrapping_add(to_copy as u32)); + } + } + + /// Write bytes to an already mapped virtual region without advancing the heap. + /// Callers must ensure the range is mapped and writable. + pub fn write_bytes(&self, start: VirtualAddress, data: &[u8]) { + self.copy_into_backing(start, data, MemoryAccessKind::Store); + } +} + +impl Sv32PageTable for Sv32Memory { + fn page_size(&self) -> usize { + self.page_size + } + + fn read_pte(&self, phys_addr: usize) -> Option { + self.read_pte(phys_addr) + } + + fn write_pte(&self, phys_addr: usize, val: u32) { + self.write_pte(phys_addr, val); + } + + fn alloc_frame(&self) -> Option { + self.allocate_frame().map(|ppn| ppn as u32) + } + + fn zero_frame(&self, ppn: u32) { + self.zero_frame(ppn as usize); + } +} + +impl MMU for Sv32Memory { + fn mem(&self) -> Ref> { + self.backing.borrow() + } + + fn mem_slice( + &self, + start: VirtualAddress, + end: VirtualAddress, + ) -> Option> { + if start.as_usize() > end.as_usize() { + return None; + } + let len = end.as_usize().saturating_sub(start.as_usize()); + let phys_start = self.translate(start, MemoryAccessKind::Load)?; + let last_va = VirtualAddress(end.as_u32().saturating_sub(1)); + let phys_last = self.translate(last_va, MemoryAccessKind::Load)?; + // Ensure the range is physically contiguous. + if phys_last + 1 != phys_start + len { + return None; + } + let backing = self.backing.borrow(); + Some(std::cell::Ref::map(backing, move |v| { + &v[phys_start..phys_start + len] + })) + } + + fn store_u16( + &self, + addr: VirtualAddress, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 2) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); + } else { + return false; + } + true + } + + fn store_u32( + &self, + addr: VirtualAddress, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 4) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); + } else { + return false; + } + true + } + + fn store_u8( + &self, + addr: VirtualAddress, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 1) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset] = val; + } else { + return false; + } + true + } + + fn load_u32( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u32::from_le_bytes( + backing[offset..offset + 4].try_into().unwrap(), + )) + } + + fn load_byte( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 1) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(backing[offset]) + } + + fn load_halfword( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 2) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u16::from_le_bytes( + backing[offset..offset + 2].try_into().unwrap(), + )) + } + + fn load_word( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u32::from_le_bytes( + backing[offset..offset + 4].try_into().unwrap(), + )) + } +} + +impl API for Sv32Memory { + fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { + Sv32Memory::map_range(self, start, len, perms); + } + + fn current_root(&self) -> usize { + self.root_ppn() + } + + fn size(&self) -> usize { + self.total_size() + } + + fn offset(&self, addr: VirtualAddress) -> usize { + addr.as_usize() + } + + fn set_satp(&self, satp: u32) { + self.satp.set(satp & SV32_SATP_PPN_MASK); + } + + fn satp(&self) -> u32 { + self.satp.get() + } + + fn stack_top(&self) -> VirtualAddress { + VirtualAddress(self.total_size() as u32) + } +} diff --git a/crates/vm/src/memory_page.rs b/crates/vm/src/memory_page.rs deleted file mode 100644 index 23ab24a..0000000 --- a/crates/vm/src/memory_page.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::rc::Rc; -use std::cell::{RefCell, Cell}; -use std::convert::TryInto; - -#[derive(Debug, Clone)] -pub struct MemoryPage { - mem: Rc>>, - pub next_heap: Cell, - pub base_address: usize, // New: base address for guest memory mapping -} - -pub const HEAP_PTR_OFFSET: u32 = 0x100; - -impl MemoryPage { - pub fn new_with_base(memory_size: usize, base_address: usize) -> Self { - Self { - mem: Rc::new(RefCell::new(vec![0u8; memory_size])), - next_heap: Cell::new(0), - base_address, - } - } - pub fn new(memory_size: usize) -> Self { - Self::new_with_base(memory_size, 0) - } - - pub fn mem(&self) -> std::cell::Ref> { - self.mem.borrow() - } - - pub fn size(&self) -> usize { - let mem = self.mem(); - mem.len() - } - - pub fn offset(&self, addr: usize) -> usize { - addr.checked_sub(self.base_address).expect("Address below base_address") - } - - pub fn store_u16(&self, addr: usize, val: u16) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 2 > mem.len() { - panic!("store u16 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); - } - - pub fn store_u32(&self, addr: usize, val: u32) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 4 > mem.len() { - panic!("store u32 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); - } - - pub fn store_u8(&self, addr: usize, val: u8) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset >= mem.len() { - panic!("store u8 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset] = val; - } - - pub fn load_u32(&self, addr: usize) -> u32 { - let offset = self.offset(addr); - let mem = self.mem.borrow(); - if offset + 4 > mem.len() { - panic!("load u32 out of bounds: addr = 0x{:08x}", addr); - } - u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap()) - } - - pub fn load_byte(&self, addr: usize) -> u8 { - let offset = self.offset(addr); - let mem = self.mem.borrow(); - mem[offset] - } - - pub fn load_halfword(&self, addr: usize) -> u16 { - let offset = self.offset(addr); - let mem = self.mem.borrow(); - u16::from_le_bytes(mem[offset..offset + 2].try_into().unwrap()) - } - - pub fn load_word(&self, addr: usize) -> u32 { - let offset = self.offset(addr); - let mem = self.mem.borrow(); - u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap()) - } - - pub fn store_byte(&mut self, addr: usize, value: u8) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset] = value; - } - - pub fn store_halfword(&mut self, addr: usize, value: u16) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); - } - - pub fn store_word(&mut self, addr: usize, value: u32) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); - } - - pub fn mem_slice(&self, start: usize, end: usize) -> Option> { - let start_offset = self.offset(start); - let end_offset = self.offset(end); - let mem_ref = self.mem.borrow(); - if end_offset > mem_ref.len() || start_offset > end_offset { - return None; - } - Some(std::cell::Ref::map(mem_ref, move |v| &v[start_offset..end_offset])) - } - - pub fn write_code(&mut self, start_addr: usize, code: &[u8]) { - let start_offset = self.offset(start_addr); - let mut mem = self.mem.borrow_mut(); - let end = start_offset + code.len(); - mem[start_offset..end].copy_from_slice(code); - - // set heap pointer - self.next_heap = Cell::new(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); - } - - pub fn alloc_on_heap(&self, data: &[u8]) -> u32 { - let mut addr = self.next_heap.get(); - - // Align to 4 bytes (or 8 if you're storing u64s) - let align = 8; - addr = (addr + (align - 1)) & !(align - 1); - - let end = addr + data.len() as u32; - assert!(end as usize <= self.size(), "Out of memory: trying to allocate {} bytes, but only {} bytes available", data.len(), self.size() - addr as usize); - - self.mem.borrow_mut()[addr as usize..end as usize].copy_from_slice(data); - self.next_heap.set(end); - - addr - } - - pub fn stack_top(&self) -> u32 { - self.size() as u32 - } -} - -impl Default for MemoryPage { - fn default() -> Self { - MemoryPage::new(4096) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_offset_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - assert_eq!(mem.offset(0), 0); - assert_eq!(mem.offset(100), 100); - assert_eq!(mem.offset(1023), 1023); - } - - #[test] - fn test_offset_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - assert_eq!(mem.offset(base), 0); - assert_eq!(mem.offset(base + 100), 100); - assert_eq!(mem.offset(base + 1023), 1023); - } - - #[test] - #[should_panic(expected = "Address below base_address")] - fn test_offset_below_base_panics() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.offset(base - 1); - } -} diff --git a/crates/vm/src/metering.rs b/crates/vm/src/metering.rs new file mode 100644 index 0000000..e35c6a2 --- /dev/null +++ b/crates/vm/src/metering.rs @@ -0,0 +1,78 @@ +use crate::instruction::Instruction; + +/// Outcome returned by metering hooks to indicate whether execution should continue. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MeterResult { + Continue, + Halt, +} + +/// Identifies the type of memory access being charged. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryAccessKind { + Load, + Store, + Atomic, + ReservationLoad, + ReservationStore, +} + +/// Pluggable metering interface. Implementors can account for gas or other resource +/// usage without changing the VM core. All methods default to no-op/continue. +pub trait Metering: std::fmt::Debug { + /// Called on instruction execution. + fn on_instruction(&mut self, _pc: u32, _instr: &Instruction, _size: u8) -> MeterResult { + MeterResult::Continue + } + + /// Called for each memory access with its width. + fn on_memory_access( + &mut self, + _kind: MemoryAccessKind, + _addr: usize, + _bytes: usize, + ) -> MeterResult { + MeterResult::Continue + } + + /// Called when a syscall is dispatched (before handler-specific work). + fn on_syscall(&mut self, _call_id: u32, _args: &[u32; 6]) -> MeterResult { + MeterResult::Continue + } + + /// Called for syscall-specific data-dependent charges (payload copies, etc.). + fn on_syscall_data(&mut self, _call_id: u32, _bytes: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when a general-purpose register is read. + fn on_register_read(&mut self, _reg: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when a general-purpose register is written. + fn on_register_write(&mut self, _reg: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when the program counter is updated. + fn on_pc_update(&mut self, _old_pc: u32, _new_pc: u32) -> MeterResult { + MeterResult::Continue + } + + /// Called when guest requests heap allocation. + fn on_alloc(&mut self, _bytes: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when guest requests a program call; input_bytes covers calldata size. + fn on_call(&mut self, _input_bytes: usize) -> MeterResult { + MeterResult::Continue + } +} + +/// Default metering that performs no accounting. +#[derive(Debug, Default)] +pub struct NoopMeter; + +impl Metering for NoopMeter {} diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs deleted file mode 100644 index 906aa9f..0000000 --- a/crates/vm/src/sys_call.rs +++ /dev/null @@ -1,577 +0,0 @@ -use crate::memory_page::{MemoryPage, HEAP_PTR_OFFSET}; -use storage::Storage; -use crate::registers::Register; -use std::rc::Rc; -use core::cell::RefCell; -use crate::host_interface::HostInterface; -use std::any::Any; -use types::result::RESULT_SIZE; -use core::fmt::Write; - -/// System call IDs for the VM. -pub const SYSCALL_STORAGE_GET: u32 = 1; -pub const SYSCALL_STORAGE_SET: u32 = 2; -pub const SYSCALL_PANIC: u32 = 3; -pub const SYSCALL_LOG: u32 = 4; -pub const SYSCALL_CALL_PROGRAM: u32 = 5; -pub const SYSCALL_FIRE_EVENT: u32 = 6; -pub const SYSCALL_ALLOC: u32 = 7; -pub const SYSCALL_DEALLOC: u32 = 8; -pub const SYSCALL_TRANSFER: u32 = 9; -pub const SYSCALL_BALANCE: u32 = 10; -/// Represents different types of arguments that can be passed to system calls. -/// -/// EDUCATIONAL: This enum demonstrates how to handle different data types -/// in system calls. In real operating systems, system calls need to handle -/// various data types safely. -enum Arg { - U32(u32), // 32-bit unsigned integer - F32(f32), // 32-bit floating point - Char(char), // Single character - Str(String), // String (owned) - Bytes(Vec), // Raw bytes -} - -pub trait SyscallHandler: std::fmt::Debug { - fn handle_syscall( - &mut self, - call_id: u32, - args: [u32; 6], - memory: Rc>, - storage: Rc>, - host: &mut Box, - regs: &mut [u32; 32], - ) -> (u32, bool); - fn as_any(&self) -> &dyn Any; -} - -pub struct DefaultSyscallHandler { - verbose_writer: Option>>, -} - -impl std::fmt::Debug for DefaultSyscallHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DefaultSyscallHandler") - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "")) - .finish() - } -} - -impl DefaultSyscallHandler { - pub fn new() -> Self { - Self { - verbose_writer: None, - } - } - - pub fn with_writer(writer: Option>>) -> Self { - Self { - verbose_writer: writer, - } - } -} - -impl SyscallHandler for DefaultSyscallHandler { - fn handle_syscall( - &mut self, - call_id: u32, - args: [u32; 6], - memory: Rc>, - storage: Rc>, - host: &mut Box, - regs: &mut [u32; 32], - ) -> (u32, bool) { - let result = match call_id { - SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage), - SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage), - SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), - SYSCALL_LOG => self.sys_log(args, memory), - SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host), - SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host), - SYSCALL_ALLOC => self.sys_alloc(args, memory), - SYSCALL_DEALLOC => self.sys_dealloc(args, memory), - SYSCALL_TRANSFER => self.sys_transfer(args, memory, host), - SYSCALL_BALANCE => self.sys_balance(args, memory, host), - _ => { - panic!("Unknown syscall: {}", call_id); - } - }; - (result, true) - } - fn as_any(&self) -> &dyn Any { - self - } -} - -impl DefaultSyscallHandler { - pub fn sys_fire_event(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box,) -> u32 { - // EDUCATIONAL: Extract key pointer and length from arguments - let ptr = args[0] as usize; - let len = args[1] as usize; - - let borrowed_memory = memory.borrow(); - - // EDUCATIONAL: Safely read the key from memory - // EDUCATIONAL: Create a limited scope to avoid borrow checker issues - let event_bytes = match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(r) => r, - None => panic!("invalid memory access"), // Invalid memory access - }; - - host.fire_event(event_bytes.to_vec()); - 0 - } - - fn sys_storage_get(&mut self, args: [u32; 6], memory: Rc>, storage: Rc>) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; - let key_ptr = args[2] as usize; - let key_len = args[3] as usize; - - let borrowed_memory = memory.borrow(); - - // Parse domain - let domain_slice = { - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { - Some(r) => r, - None => { - println!("❌ Storage GET - Invalid domain memory access: ptr={}, len={}", domain_ptr, domain_len); - return 0; - } - }; - domain_slice_ref.as_ref().to_vec() - }; - let domain = match core::str::from_utf8(&domain_slice) { - Ok(s) => s, - Err(_) => { - println!("❌ Storage GET - Invalid UTF-8 in domain: {:?}", domain_slice); - return 0; - } - }; - - // Parse key - let key_slice = { - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { - Some(r) => r, - None => { - println!("❌ Storage GET - Invalid key memory access: ptr={}, len={}", key_ptr, key_len); - return 0; - } - }; - key_slice_ref.as_ref().to_vec() - }; - // Convert binary key to hex string for storage - let key = key_slice.iter().map(|b| format!("{:02x}", b)).collect::>().join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(&key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - if let Some(value) = storage.borrow().get(domain, &key) { - let mut buf = (value.len() as u32).to_le_bytes().to_vec(); - buf.extend_from_slice(value.as_slice()); - let addr = borrowed_memory.alloc_on_heap(&buf); - println!("βœ… Found value for domain: '{}', Key: '{}'", domain, display_key); - return addr; - } else { - println!("❌ No value found for domain: '{}', key: '{}'", domain, display_key); - 0 - } - } - - fn sys_storage_set(&mut self, args: [u32; 6], memory: Rc>, storage: Rc>) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; - let key_ptr = args[2] as usize; - let key_len = args[3] as usize; - let val_ptr = args[4] as usize; - let val_len = args[5] as usize; - - let borrowed_memory = memory.borrow(); - - // Parse domain - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { - Some(r) => r, - None => { - println!("❌ Storage SET - Invalid domain memory access: ptr={}, len={}", domain_ptr, domain_len); - return 0; - } - }; - let domain_slice = domain_slice_ref.as_ref(); - let domain = match core::str::from_utf8(domain_slice) { - Ok(s) => s, - Err(_) => { - println!("❌ Storage SET - Invalid UTF-8 in domain: {:?}", domain_slice); - return 0; - } - }; - - // Parse key - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { - Some(r) => r, - None => { - println!("❌ Storage SET - Invalid key memory access: ptr={}, len={}", key_ptr, key_len); - return 0; - } - }; - let key_slice = key_slice_ref.as_ref(); - // Convert binary key to hex string for storage - let key = key_slice.iter().map(|b| format!("{:02x}", b)).collect::>().join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - // Parse value - let value_slice_ref = match borrowed_memory.mem_slice(val_ptr, val_ptr + val_len) { - Some(r) => r, - None => { - println!("❌ Storage SET - Invalid value memory access: ptr={}, len={}", val_ptr, val_len); - return 0; - } - }; - let value_slice = value_slice_ref.as_ref(); - - println!("πŸ’Ύ Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", - domain, display_key, value_slice, value_slice.len()); - - storage.borrow_mut().set(domain, &key, value_slice.to_vec()); - 0 - } - - fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Rc>) -> u32 { - let msg_ptr = regs[Register::A0 as usize] as usize; - let msg_len = regs[Register::A1 as usize] as usize; - let msg = memory - .borrow() - .mem_slice(msg_ptr, msg_ptr + msg_len) - .map(|bytes| { - String::from_utf8_lossy(&bytes).into_owned() - }) - .unwrap_or_else(|| "".to_string()); - panic!("πŸ”₯ Guest panic: {}", msg); - } - - fn sys_log(&mut self, args: [u32; 6], memory: Rc>) -> u32 { - let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; - let borrowed_memory = memory.borrow(); - let fmt_slice = match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { - Some(s) => s, - None => { - println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); - return 0; - } - }; - let fmt_bytes = fmt_slice.as_ref(); - let fmt = match core::str::from_utf8(fmt_bytes) { - Ok(s) => s, - Err(e) => { - println!("⚠️ invalid UTF-8 in format string"); - println!("πŸ“¦ bytes: {:?}", fmt_bytes); - println!("❌ error: {}", e); - return 0; - } - }; - let args_bytes_slice = borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); - let args_bytes_holder; - let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { - args_bytes_holder = slice; - args_bytes_holder.as_ref() - } else { - b"" - }; - let raw_args: Vec = args_bytes - .chunks_exact(4) - .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect(); - let mut args: Vec = Vec::new(); - let mut raw_iter = raw_args.into_iter(); - let mut chars = fmt.chars().peekable(); - while let Some(c) = chars.next() { - if c != '%' { continue; } - let spec: char = chars.next().unwrap_or('%'); - let mut next = || raw_iter.next().unwrap_or(0); - match spec { - 'd' | 'u' | 'x' => args.push(Arg::U32(next())), - 'f' => args.push(Arg::F32(f32::from_bits(next()))), - 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), - 's' => { - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - let s_ptr = core::str::from_utf8(slice.as_ref()); - args.push(match s_ptr { - Ok(s) => Arg::Str(s.to_string()), - Err(_) => Arg::Str("".to_string()), - }); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'b' => { - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'a' => { - // Array of u32s - let ptr = next() as usize; - let len = next() as usize; - let byte_len = len * 4; // u32 is 4 bytes - match borrowed_memory.mem_slice(ptr, ptr + byte_len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'A' => { - // Array of u8s - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - _ => args.push(Arg::Str("".to_string())), - } - } - let mut output = String::new(); - let mut args_iter = args.iter(); - let mut fmt_chars = fmt.chars().peekable(); - while let Some(c) = fmt_chars.next() { - if c == '%' { - match fmt_chars.next() { - Some('d') | Some('u') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), - _ => output.push_str(""), - }, - Some('x') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), - _ => output.push_str(""), - }, - Some('f') => match args_iter.next() { - Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), - _ => output.push_str(""), - }, - Some('c') => match args_iter.next() { - Some(Arg::Char(c)) => output.push(*c), - _ => output.push_str(""), - }, - Some('s') => match args_iter.next() { - Some(Arg::Str(s)) => output.push_str(s), - _ => output.push_str(""), - }, - Some('b') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format bytes array nicely - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { output.push_str(", "); } - output.push_str(&format!("0x{:02x}", byte)); - } - output.push(']'); - }, - _ => output.push_str(""), - }, - Some('a') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u32 array (bytes interpreted as u32s) - output.push('['); - for (i, chunk) in b.chunks_exact(4).enumerate() { - if i > 0 { output.push_str(", "); } - let val = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - output.push_str(&format!("{}", val)); - } - output.push(']'); - }, - _ => output.push_str(""), - }, - Some('A') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u8 array - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { output.push_str(", "); } - output.push_str(&format!("{}", byte)); - } - output.push(']'); - }, - _ => output.push_str(""), - }, - Some('%') => output.push('%'), - Some(_) | None => output.push_str("<%?>"), - } - } else { - output.push(c); - } - } - match &self.verbose_writer { - Some(writer) => { - let _ = writeln!(writer.borrow_mut(), "πŸ“œ Guest Log: {}", output); - } - None => { - println!("πŸ“œ Guest Log: {}", output); - } - } - 0 - } - - fn sys_call_program(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { - let to_ptr = args[0] as usize; - let from_ptr = args[1] as usize; - let input_ptr = args[2] as usize; - let input_len = args[3] as usize; - let result_ptr: u32; - let page_index: usize; - { - let borrowed_memory = memory.borrow(); - let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { - Some(r) => r, - None => return 0, - }; - let from_slice = match borrowed_memory.mem_slice(from_ptr, from_ptr + 20) { - Some(r) => r, - None => return 0, - }; - let input_slice = match borrowed_memory.mem_slice(input_ptr, input_ptr + input_len) { - Some(r) => r, - None => return 0, - }; - let mut to_bytes = [0u8; 20]; - let mut from_bytes = [0u8; 20]; - to_bytes.copy_from_slice(&to_slice); - from_bytes.copy_from_slice(&from_slice); - let input_vec = input_slice.to_vec(); - (result_ptr, page_index) = host.call_program(from_bytes, to_bytes, input_vec); - } - { - let borrowed_memory = memory.borrow_mut(); - let result_bytes = match host.read_memory_page(page_index, result_ptr, RESULT_SIZE) { - Some(b) => b, - None => return 0, - }; - borrowed_memory.alloc_on_heap(&result_bytes) - } - } - - fn sys_alloc(&mut self, args: [u32; 6], memory: Rc>) -> u32 { - let size = args[0] as usize; // A0 register - let align = args[1] as usize; // A1 register - - if size == 0 { - println!("VM Alloc: Invalid size 0"); - return 0; - } - - // Validate alignment (must be power of 2) - if align == 0 || (align & (align - 1)) != 0 { - println!("VM Alloc: Invalid alignment {}", align); - return 0; - } - - let current_heap = memory.borrow().next_heap.get(); - - // Initialize heap pointer if not set (no code has been written) - if current_heap == 0 { - memory.borrow().next_heap.set(HEAP_PTR_OFFSET); - } - - // Allocate aligned memory on heap - let data = vec![0u8; size]; - let ptr = memory.borrow().alloc_on_heap(&data); - - if ptr == 0 { - println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); - return 0; - } - - // Check if allocated address meets alignment requirements - if (ptr as usize) % align != 0 { - // Re-allocate with enough space for alignment - let total_size = size + align - 1; - let padded_data = vec![0u8; total_size]; - let padded_ptr = memory.borrow().alloc_on_heap(&padded_data); - if padded_ptr == 0 { - println!("VM Alloc: Out of memory, failed to allocate {} bytes for alignment", total_size); - return 0; - } - // Return properly aligned pointer within the allocated region - let aligned_ptr = ((padded_ptr as usize + align - 1) & !(align - 1)) as u32; - return aligned_ptr; - } - - ptr - } - - fn sys_dealloc(&mut self, _args: [u32; 6], _memory: Rc>) -> u32 { - // Note: This VM uses a simple bump allocator, so we can't actually free memory - // In a real VM, you'd implement a proper allocator with free lists - // For now, this is a no-op since the memory will be reclaimed when the VM exits - 0 - } - - fn sys_transfer(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { - // args: a2=to ptr, a3=value_lo, a4=value_hi - let to_ptr = args[1] as usize; - let value_lo = args[2] as u64; - let value_hi = args[3] as u64; - let value = value_lo | (value_hi << 32); - - let borrowed = memory.borrow(); - let to_slice = borrowed.mem_slice(to_ptr, to_ptr + 20).expect("invalid to ptr"); - - let mut to = [0u8; 20]; - to.copy_from_slice(to_slice.as_ref()); - - if host.transfer(to, value) { 0 } else { 1 } - } - - fn sys_balance(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { - // args: a1 = address pointer (20 bytes) - let addr_ptr = args[0] as usize; - let addr = { - let borrowed = memory.borrow(); - let addr_slice = borrowed.mem_slice(addr_ptr, addr_ptr + 20).expect("invalid addr ptr"); - let mut addr = [0u8; 20]; - addr.copy_from_slice(addr_slice.as_ref()); - addr - }; - - let bal = host.balance(addr); - memory.borrow().alloc_on_heap(&bal.to_le_bytes()) - } -} diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 7c77444..f1c6cea 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,192 +1,89 @@ -use std::rc::Rc; -use core::cell::RefCell; use crate::cpu::CPU; +use crate::memory::{API, Memory}; +use crate::metering::Metering; use crate::registers::Register; -use crate::memory_page::{MemoryPage}; -use storage::{Storage}; -use crate::host_interface::HostInterface; -use crate::sys_call::{SyscallHandler, DefaultSyscallHandler}; +use std::rc::Rc; /// Represents a complete RISC-V virtual machine. -/// +/// /// EDUCATIONAL PURPOSE: This struct encapsulates all the components needed -/// to run a virtual machine: CPU, memory, and persistent storage. It provides +/// to run a virtual machine: CPU and memory. It provides /// a high-level interface for VM operations while hiding the complexity of /// the underlying components. -/// +/// /// VM ARCHITECTURE OVERVIEW: /// - CPU: Executes RISC-V instructions /// - Memory: Provides RAM for the running program -/// - Storage: Persistent storage for data that survives between runs -/// -/// MEMORY MANAGEMENT: Uses Rc> for shared mutable access to memory -/// and storage, allowing the VM to manage resources efficiently while maintaining -/// Rust's safety guarantees. +/// +/// MEMORY MANAGEMENT: Uses Rc to share a trait-backed memory implementation, +/// allowing the VM to manage resources efficiently while maintaining Rust's +/// safety guarantees. #[derive(Debug)] pub struct VM { /// The CPU that executes RISC-V instructions pub cpu: CPU, - - /// Shared reference to the VM's memo ry (RAM) - pub memory: Rc>, - - /// Shared reference to persistent storage - pub storage: Rc>, - pub host: Box, + /// Shared reference to the VM's memory (RAM) + pub memory: Memory, + } impl VM { - /// Creates a new virtual machine with the specified memory, storage, and host, using the default syscall handler. - pub fn new( - memory: Rc>, - storage: Rc>, - host: Box, - ) -> Self { - Self::new_with_syscall_handler(memory, storage, host, Box::new(DefaultSyscallHandler::new())) - } - - /// Creates a new virtual machine with a writer for logging output. - pub fn new_with_writer( - memory: Rc>, - storage: Rc>, - host: Box, - writer: Option>>, - ) -> Self { - Self::new_with_syscall_handler(memory, storage, host, Box::new(DefaultSyscallHandler::with_writer(writer))) - } - - /// Creates a new virtual machine with a custom syscall handler. - /// This is useful for testing or custom environments. - pub fn new_with_syscall_handler( - memory: Rc>, - storage: Rc>, - host: Box, - syscall_handler: Box, - ) -> Self { - let mut cpu = CPU::new(syscall_handler); - cpu.regs[Register::Sp as usize] = memory.borrow().stack_top(); - Self { cpu, memory, storage, host } - } - - /// Loads program code into memory and sets the starting address. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how programs are loaded into - /// a VM. In real systems, this would involve loading from disk, parsing - /// executable formats, and setting up memory protection. - /// - /// PARAMETERS: - /// - alloc_add: The address where the code should be allocated in memory - /// - start_addr: Where the program should start executing from - /// - code: The binary program code to load - /// - /// MEMORY LAYOUT: Programs are typically loaded at specific addresses - /// to ensure proper alignment and to avoid conflicts with system memory. - pub fn set_code(&mut self, alloc_add: u32, start_addr: u32, code: &[u8]) { - // EDUCATIONAL: Write the program code to memory starting at address 0 - self.memory.borrow_mut().write_code(alloc_add as usize, code); - - // EDUCATIONAL: Set the program counter to the starting address - self.cpu.pc = start_addr; + /// Creates a new virtual machine with the specified memory. + pub fn new(memory: Memory) -> Self { + let mut cpu = CPU::new(); + cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); + let satp = memory.satp(); + cpu.set_satp(&memory, satp); + Self { cpu, memory } } - /// Allocates memory on the heap and writes data to it. - /// - /// EDUCATIONAL PURPOSE: This demonstrates dynamic memory allocation in a VM. - /// Programs need to allocate memory for variables, arrays, and other data - /// structures at runtime. - /// - /// HEAP MANAGEMENT: The VM maintains a heap pointer that moves forward - /// as memory is allocated. This is a simple but effective allocation strategy. - /// - /// RETURN VALUE: Returns the address where the data was written - pub fn alloc_and_write(&mut self, data: &[u8]) -> u32 { - self.memory.borrow_mut().alloc_on_heap(data) + /// Installs a metering implementation on the underlying CPU. + pub fn set_metering(&mut self, metering: Box) { + self.cpu.set_metering(metering); } - - /// Sets a register to point to data in memory. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how to pass data to programs - /// running in the VM. Instead of copying data into registers (which are - /// limited in size), we store the data in memory and pass the address. - /// - /// PARAMETER PASSING: This is how we pass strings, arrays, and other - /// large data structures to programs. The register contains a pointer - /// to the actual data in memory. - /// - /// DEBUG OUTPUT: The function prints information about what it's doing, - /// which is helpful for understanding VM behavior during development. - pub fn set_reg_to_data(&mut self, reg: Register, data: &[u8]) -> u32 { - // EDUCATIONAL: Allocate memory and write the data - let addr = self.alloc_and_write(data); - - // EDUCATIONAL: Set the register to point to the data - self.cpu.regs[reg as usize] = addr; - - // EDUCATIONAL: Debug output to help understand what's happening - println!( - "πŸ“₯ set reg x{} to addr 0x{:08x} (len = {})", - reg as u32, - addr, - data.len() - ); - println!( - "πŸ“¦ data written to 0x{:08x}: {:02x?}", - addr, - data - ); - - addr - } - - /// Sets a register to a 32-bit value. - /// - /// EDUCATIONAL PURPOSE: This is used for passing small values (like - /// integers) directly to programs. For larger data, use set_reg_to_data. - /// - /// USAGE: Typically used for passing function parameters, flags, or - /// other small values that fit in a single register. pub fn set_reg_u32(&mut self, reg: Register, data: u32) { self.cpu.regs[reg as usize] = data; } + pub fn memory_api(&self) -> Rc { + self.memory.clone() as Rc + } + /// Dumps the entire memory contents for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates memory inspection tools that /// are essential for debugging VM programs. It shows both hex and ASCII /// representations of memory contents. - /// + /// /// DEBUGGING: Memory dumps are crucial for understanding what's happening /// when programs don't work as expected. They show the actual data in memory. pub fn dump_all_memory(&self) { - self.dump_memory(0, self.memory.borrow().mem().len()); + self.dump_memory(0, self.memory.mem().len()); } /// Dumps a specific range of memory for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This function provides a detailed view of memory /// contents, showing both hexadecimal and ASCII representations. This is /// similar to tools like 'hexdump' or 'xxd' in Unix systems. - /// + /// /// OUTPUT FORMAT: /// - Address in hexadecimal /// - 16 bytes of data in hex format /// - ASCII representation (printable characters only) /// - Heap pointer location - /// + /// /// MEMORY LAYOUT: The output shows how memory is organized, including /// where the heap pointer is and what data is stored where. pub fn dump_memory(&self, start: usize, end: usize) { - let borrowed_memory = self.memory.borrow(); + let borrowed_memory = self.memory.as_ref(); // EDUCATIONAL: Validate memory range to prevent errors assert!(start < end, "invalid memory range"); assert!(end <= borrowed_memory.mem().len(), "range out of bounds"); - // EDUCATIONAL: Show heap pointer for context - let next_heap = borrowed_memory.next_heap.get(); println!("--- Memory Dump ---"); - println!("Next heap pointer: 0x{:08x}", next_heap); // EDUCATIONAL: Display memory in 16-byte lines for addr in (start..end).step_by(16) { @@ -197,7 +94,8 @@ impl VM { let hex_str = hex.join(" "); // EDUCATIONAL: Convert bytes to ASCII (printable characters only) - let ascii: String = line.iter() + let ascii: String = line + .iter() .map(|&b| if b.is_ascii_graphic() { b as char } else { '.' }) .collect(); @@ -207,14 +105,14 @@ impl VM { } /// Dumps the current state of all CPU registers for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates register inspection, which is /// essential for understanding program state and debugging issues. - /// + /// /// RISC-V REGISTER CONVENTIONS: The output shows both register numbers /// and their ABI (Application Binary Interface) names, which helps /// understand how registers are used in RISC-V programs. - /// + /// /// REGISTER USAGE: /// - x0 (zero): Always zero /// - x1 (ra): Return address @@ -227,10 +125,9 @@ impl VM { // EDUCATIONAL: RISC-V ABI register names for easier understanding const ABI_NAMES: [&str; 32] = [ - "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", - "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5", - "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", - "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6", + "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3", + "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", + "t3", "t4", "t5", "t6", ]; // EDUCATIONAL: Display each register with its name and value @@ -246,20 +143,20 @@ impl VM { } /// Starts program execution without initializing registers or setting up state. - /// + /// /// EDUCATIONAL PURPOSE: This is the main execution loop of the VM. It /// continuously fetches, decodes, and executes instructions until the /// program halts or encounters an error. - /// + /// /// EXECUTION LOOP: This implements the classic fetch-decode-execute cycle /// that all CPUs follow. The loop continues until the CPU returns false, /// indicating that execution should stop. - /// + /// /// ASSUMPTIONS: This function assumes the VM is already properly configured /// with code loaded and registers set up. For a complete VM, you'd typically /// call this after setting up the initial state. pub fn raw_run(&mut self) { // EDUCATIONAL: Main execution loop - fetch, decode, execute - while self.cpu.step(Rc::clone(&self.memory), Rc::clone(&self.storage), &mut self.host) {} + while self.cpu.step(Rc::clone(&self.memory)) {} } -} \ No newline at end of file +} diff --git a/crates/vm/tests/allocator_test.rs b/crates/vm/tests/allocator_test.rs deleted file mode 100644 index c96907c..0000000 --- a/crates/vm/tests/allocator_test.rs +++ /dev/null @@ -1,148 +0,0 @@ -use vm::sys_call::{SyscallHandler, DefaultSyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; -use vm::{memory_page, host_interface}; -use storage::Storage; -use std::rc::Rc; -use std::cell::RefCell; - -#[test] -fn test_allocator_syscalls() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - - // Test SYSCALL_ALLOC - let args = [ - 1024, // size - 8, // alignment - 0, 0, 0, 0 - ]; - let mut regs = [0u32; 32]; - let (result, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - ); - - println!("βœ… SYSCALL_ALLOC returned pointer: 0x{:08x}", result); - assert_ne!(result, 0); // Should return valid pointer - - // Test SYSCALL_DEALLOC (no-op but should not crash) - let dealloc_args = [ - result, // pointer to deallocate - 1024, // size - 0, 0, 0, 0 - ]; - let (dealloc_result, _) = syscall_handler.handle_syscall( - SYSCALL_DEALLOC, - dealloc_args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - ); - - println!("βœ… SYSCALL_DEALLOC returned: {}", dealloc_result); - assert_eq!(dealloc_result, 0); // Should return 0 (success) -} - -#[test] -fn test_multiple_allocations() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - - let mut pointers = Vec::new(); - - // Allocate multiple blocks - for i in 0..5 { - let size = 64 + i * 32; // Different sizes - let args = [size, 4, 0, 0, 0, 0]; // 4-byte alignment - - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - ); - - println!("βœ… Allocation {}: size={}, ptr=0x{:08x}", i, size, ptr); - assert_ne!(ptr, 0); - pointers.push(ptr); - } - - // Verify pointers are different and properly aligned - for (i, &ptr) in pointers.iter().enumerate() { - assert!(ptr % 4 == 0, "Allocation {} not aligned: 0x{:08x}", i, ptr); - } - - // Verify no overlapping pointers (simple check) - for i in 0..pointers.len() { - for j in i+1..pointers.len() { - assert_ne!(pointers[i], pointers[j], "Duplicate pointers: 0x{:08x}", pointers[i]); - } - } -} - -#[test] -fn test_alignment_requirements() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - - // Test various alignments - let alignments = [1, 2, 4, 8, 16]; - - for &align in &alignments { - let args = [256, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - ); - - println!("βœ… Alignment test: align={}, ptr=0x{:08x}", align, ptr); - assert_ne!(ptr, 0); - assert!(ptr as usize % align == 0, - "Pointer 0x{:08x} not aligned to {} bytes", ptr, align); - } -} - -#[test] -fn test_invalid_alignment() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - - // Test invalid alignments (not powers of 2) - let invalid_alignments = [0, 3, 5, 6, 7, 9]; - - for &align in &invalid_alignments { - let args = [100, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - ); - - println!("βœ… Invalid alignment test: align={}, ptr=0x{:08x}", align, ptr); - assert_eq!(ptr, 0, "Should return null for invalid alignment {}", align); - } -} \ No newline at end of file diff --git a/crates/vm/tests/memory_page_offset.rs b/crates/vm/tests/memory_page_offset.rs deleted file mode 100644 index 27346e7..0000000 --- a/crates/vm/tests/memory_page_offset.rs +++ /dev/null @@ -1,59 +0,0 @@ -use vm::memory_page::MemoryPage; -use vm::vm::VM; -use vm::sys_call::DefaultSyscallHandler; - -#[test] -fn test_offset_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - assert_eq!(mem.offset(0), 0); - assert_eq!(mem.offset(100), 100); - assert_eq!(mem.offset(1023), 1023); -} - -#[test] -fn test_offset_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - assert_eq!(mem.offset(base), 0); - assert_eq!(mem.offset(base + 100), 100); - assert_eq!(mem.offset(base + 1023), 1023); -} - -#[test] -#[should_panic(expected = "Address below base_address")] -fn test_offset_below_base_panics() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.offset(base - 1); -} - -#[test] -fn test_store_and_load_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - mem.store_u8(10, 0xAB); - assert_eq!(mem.load_byte(10), 0xAB); - mem.store_u16(20, 0xCDEF); - assert_eq!(mem.load_halfword(20), 0xCDEF); - mem.store_u32(30, 0x12345678); - assert_eq!(mem.load_u32(30), 0x12345678); -} - -#[test] -fn test_store_and_load_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.store_u8(base + 10, 0xAB); - assert_eq!(mem.load_byte(base + 10), 0xAB); - mem.store_u16(base + 20, 0xCDEF); - assert_eq!(mem.load_halfword(base + 20), 0xCDEF); - mem.store_u32(base + 30, 0x12345678); - assert_eq!(mem.load_u32(base + 30), 0x12345678); -} - -#[test] -fn test_store_and_load_at_offset_zero() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.store_u8(base, 0xAA); - assert_eq!(mem.load_byte(base), 0xAA); -} \ No newline at end of file diff --git a/crates/vm/tests/spec_runner.rs b/crates/vm/tests/spec_runner.rs index 5ab937d..980e97d 100644 --- a/crates/vm/tests/spec_runner.rs +++ b/crates/vm/tests/spec_runner.rs @@ -3,9 +3,13 @@ use std::io::Read; use std::path::Path; +use vm::memory::{API, MMU, Perms, Sv32Memory, VirtualAddress, PAGE_SIZE}; +use vm::registers::Register; use vm::vm::VM; -mod test_syscall_handler; -use test_syscall_handler::TestSyscallHandler; + +const DEFAULT_VM_SIZE: usize = 16 * 1024 * 1024; +const STACK_SIZE: usize = 256 * 1024; +const MAX_STEPS: usize = 20_000_000; /// Tests that are skipped and the reasons why const SKIPPED_TESTS: &[(&str, &str)] = &[ @@ -43,7 +47,8 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { // Parse ELF let elf = compiler::elf::parse_elf_from_bytes(&elf_bytes)?; let (code, code_start) = elf.get_flat_code().ok_or("No code section in ELF")?; - let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((vec![], usize::MAX as u64)); + let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((vec![], u64::MAX)); + let (bss, bss_start) = elf.get_flat_bss().unwrap_or((vec![], u64::MAX)); // Get .data section if it exists let (data, data_start) = if let Some(data_section) = elf.get_section_by_name(".data") { @@ -53,55 +58,132 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { }; // Find .tohost section - if let Some(tohost_section) = elf.get_section_by_name(".tohost") { - println!(".tohost section found at addr=0x{:x}, size=0x{:x}", tohost_section.addr, tohost_section.size); + let tohost_section = if let Some(tohost_section) = elf.get_section_by_name(".tohost") { + println!( + ".tohost section found at addr=0x{:x}, size=0x{:x}", + tohost_section.addr, tohost_section.size + ); + tohost_section } else { println!(".tohost section not found, skipping..."); return Ok(()); - } + }; + let tohost_addr = tohost_section.addr; - // Set up VM memory (allocate enough to cover 0x80000000+) - let memory = std::rc::Rc::new(std::cell::RefCell::new(vm::memory_page::MemoryPage::new_with_base(0x20000, 0x80000000))); // 128KB at 0x80000000 - println!("Loading code into VM: addr=0x{:x}, size=0x{:x}", code_start, code.len()); + let mut min_base = code_start as usize; + let mut image_end = (code_start as usize) + code.len(); - // Set up VM - let storage = std::rc::Rc::new(std::cell::RefCell::new(storage::Storage::default())); - let host: Box = Box::new(vm::host_interface::NoopHost {}); - // When constructing the VM, use the test syscall handler: - let mut syscall_handler = Box::new(TestSyscallHandler::new()); - - // Set .tohost address if found - if let Some(tohost_section) = elf.get_section_by_name(".tohost") { - syscall_handler.set_tohost_addr(tohost_section.addr); - syscall_handler.set_memory(memory.clone()); + if !rodata.is_empty() { + min_base = min_base.min(rodata_start as usize); + image_end = image_end.max((rodata_start as usize) + rodata.len()); } - - // Move the handler into the VM, then extract it after run - let mut vm = VM::new_with_syscall_handler( - memory.clone(), - storage, - host, - syscall_handler, - ); - vm.cpu.verbose = false; // Set to false to reduce output for multiple tests - vm.set_code(code_start as u32, code_start as u32, &code); + if !data.is_empty() { + min_base = min_base.min(data_start); + image_end = image_end.max(data_start + data.len()); + } + if !bss.is_empty() { + min_base = min_base.min(bss_start as usize); + image_end = image_end.max((bss_start as usize) + bss.len()); + } + let tohost_start = tohost_section.addr as usize; + let tohost_end = tohost_start + (tohost_section.size as usize); + min_base = min_base.min(tohost_start); + image_end = image_end.max(tohost_end); + + let image_size = image_end + .checked_sub(min_base) + .ok_or("invalid image size")?; + let map_len = image_size + STACK_SIZE; + let total_size = map_len.max(DEFAULT_VM_SIZE); + let memory = std::rc::Rc::new(Sv32Memory::new(total_size, PAGE_SIZE)); + + println!("Loading code into VM: addr=0x{:x}, size=0x{:x}", code_start, code.len()); + println!("Mapping {:x}-{:x} (size=0x{:x})", min_base, min_base + map_len, map_len); + memory.map_range(VirtualAddress(min_base as u32), map_len, Perms::rwx_kernel()); + let mut image = vec![0u8; image_size]; + let code_off = (code_start as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); if !rodata.is_empty() { - println!("Writing rodata to memory: addr=0x{:x}, size=0x{:x}", rodata_start, rodata.len()); - memory.borrow_mut().write_code(rodata_start as usize, &rodata); + let ro_off = (rodata_start as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); } - if !data.is_empty() { - println!("Writing data to memory: addr=0x{:x}, size=0x{:x}", data_start, data.len()); - memory.borrow_mut().write_code(data_start as usize, &data); + let data_off = data_start.saturating_sub(min_base); + image[data_off..data_off + data.len()].copy_from_slice(&data); + } + if !bss.is_empty() { + let bss_off = (bss_start as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } + if !tohost_section.data.is_empty() { + let tohost_off = tohost_start.saturating_sub(min_base); + image[tohost_off..tohost_off + tohost_section.data.len()] + .copy_from_slice(tohost_section.data); } + memory.write_bytes(VirtualAddress(min_base as u32), &image); + + let stack_top = (min_base as u32) + .checked_add(map_len as u32) + .ok_or("stack top overflow")?; + let entry_point = code_start as u32; + + let mut vm = VM::new(memory.clone()); + vm.cpu.verbose = false; + vm.cpu.pc = entry_point; + vm.set_reg_u32(Register::Sp, stack_top); + let root_satp = memory.satp(); - // Run the VM println!("Running test..."); - vm.raw_run(); - println!("Test completed."); + let mut steps = 0usize; + loop { + if !vm.cpu.step(memory.clone()) { + break; + } + steps += 1; + if memory.satp() == 0 { + memory.set_satp(root_satp); + } + if steps > MAX_STEPS { + return Err("execution limit reached without tohost signal".into()); + } + let tohost_value = read_tohost_value(memory.as_ref(), tohost_addr)?; + if tohost_value != 0 { + if tohost_value == 1 { + println!("Test completed."); + return Ok(()); + } + return Err(format!("test failed (tohost=0x{:x})", tohost_value).into()); + } + } - Ok(()) + let exit_id = vm.cpu.regs[Register::A7 as usize]; + if exit_id == 93 { + let exit_code = vm.cpu.regs[Register::A0 as usize]; + if exit_code == 0 { + println!("Test completed."); + return Ok(()); + } + return Err(format!("test failed (ecall exit code={})", exit_code).into()); + } + + Err("execution halted without tohost signal".into()) +} + +fn read_tohost_value(memory: &Sv32Memory, tohost_addr: u64) -> Result> { + let addr = u32::try_from(tohost_addr).map_err(|_| "tohost address out of range")?; + let start = VirtualAddress(addr); + let end = start + .checked_add(8) + .ok_or("tohost address overflow")?; + let slice = memory + .mem_slice(start, end) + .ok_or("tohost not mapped")?; + if slice.len() < 8 { + return Err("tohost slice truncated".into()); + } + let bytes: [u8; 8] = slice[0..8].try_into()?; + Ok(u64::from_le_bytes(bytes)) } /// Discover and collect test files for a specific category @@ -264,4 +346,4 @@ fn test_riscv_spec() { } else { println!("πŸŽ‰ All tests passed successfully!"); } -} \ No newline at end of file +} diff --git a/crates/vm/tests/test_syscall_handler.rs b/crates/vm/tests/test_syscall_handler.rs deleted file mode 100644 index 90f6ed2..0000000 --- a/crates/vm/tests/test_syscall_handler.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::rc::Rc; -use core::cell::RefCell; -use vm::memory_page::MemoryPage; -use storage::Storage; -use vm::host_interface::HostInterface; -use vm::sys_call::SyscallHandler; -use vm::registers::Register; -use std::any::Any; - -/// Map RISC-V test exit codes to test case numbers -/// Formula: exit_code = (TESTNUM << 1) | 1 -/// So: TESTNUM = (exit_code - 1) >> 1 -fn exit_code_to_test_num(exit_code: u32) -> Option { - if exit_code == 0 { - None // 0 means test passed - } else if exit_code % 2 == 1 { - Some((exit_code - 1) >> 1) - } else { - None // Even exit codes are not from RVTEST_FAIL - } -} - -#[derive(Debug)] -pub struct TestSyscallHandler { - tohost_addr: u64, - memory: Option>>, -} - -impl TestSyscallHandler { - pub fn new() -> Self { - Self { tohost_addr: 0, memory: None } - } - - /// Set the address of the .tohost section - pub fn set_tohost_addr(&mut self, addr: u64) { - self.tohost_addr = addr; - } - - /// Set the memory reference (needed to read .tohost) - pub fn set_memory(&mut self, memory: Rc>) { - self.memory = Some(memory); - } -} - -pub const SYSCALL_TEST_DONE: u32 = 0; -pub const SYSCALL_TERMINATE: u32 = 93; - -impl SyscallHandler for TestSyscallHandler { - fn handle_syscall( - &mut self, - call_id: u32, - _args: [u32; 6], - memory: Rc>, - _storage: Rc>, - _host: &mut Box, - regs: &mut [u32; 32], - ) -> (u32, bool) { - let mut result = 0; - match call_id { - SYSCALL_TEST_DONE => { - // Read .tohost value - let mem_ref = self.memory.as_ref().unwrap_or(&memory); - let offset = mem_ref.borrow().offset(self.tohost_addr as usize); - let mem_guard = mem_ref.borrow(); - let mem = mem_guard.mem(); - if offset + 8 <= mem.len() { - let tohost_val = u64::from_le_bytes(mem[offset..offset+8].try_into().unwrap()); - // Use .tohost value as the test result - result = tohost_val as u32; - } else { - panic!("[TestSyscallHandler] .tohost address out of bounds"); - } - if result == 0 { - return (result, true); - } - panic!("[spec-test] FAIL: .tohost value = 0x{:x}", result); - }, - SYSCALL_TERMINATE => { - let exit_code = regs[Register::A0 as usize]; - if exit_code != 0 { - // Try to map exit code to test case number - if let Some(test_num) = exit_code_to_test_num(exit_code) { - panic!("[spec-test] FAIL: Test case {} failed (exit code {})", test_num, exit_code); - } else { - panic!("[spec-test] FAIL: Test failed with exit code {}", exit_code); - } - } - return (exit_code, false); // halt VM - }, - _ => { - panic!("Unknown syscall ID: {}", call_id); - } - } - } - fn as_any(&self) -> &dyn Any { - self - } -} \ No newline at end of file