package vm import ( "bytes" "context" "encoding/binary" "errors" "fmt" "os" "strings" "testing" "go-blockchain/blockchain" ) // ── mock host env ───────────────────────────────────────────────────────────── type mockEnv struct { state map[string][]byte balances map[string]uint64 caller string blockHeight uint64 logs []string } func newMockEnv(caller string) *mockEnv { return &mockEnv{ state: make(map[string][]byte), balances: make(map[string]uint64), caller: caller, } } func (m *mockEnv) GetState(key []byte) ([]byte, error) { v := m.state[string(key)] return v, nil } func (m *mockEnv) SetState(key, value []byte) error { m.state[string(key)] = append([]byte(nil), value...) return nil } func (m *mockEnv) GetBalance(pub string) (uint64, error) { return m.balances[pub], nil } func (m *mockEnv) Transfer(from, to string, amount uint64) error { if m.balances[from] < amount { return errors.New("insufficient balance") } m.balances[from] -= amount m.balances[to] += amount return nil } func (m *mockEnv) GetCaller() string { return m.caller } func (m *mockEnv) GetBlockHeight() uint64 { return m.blockHeight } func (m *mockEnv) GetContractTreasury() string { return "0000000000000000000000000000000000000000000000000000000000000000" } func (m *mockEnv) Log(msg string) { m.logs = append(m.logs, msg) } func (m *mockEnv) CallContract(contractID, method string, argsJSON []byte, gasLimit uint64) (uint64, error) { return 0, fmt.Errorf("CallContract not supported in test mock") } // counterWASM loads counter.wasm relative to the test file. func counterWASM(t *testing.T) []byte { t.Helper() data, err := os.ReadFile("../contracts/counter/counter.wasm") if err != nil { t.Fatalf("load counter.wasm: %v", err) } return data } // readU64State reads an 8-byte big-endian uint64 from env state. func readU64State(env *mockEnv, key string) uint64 { v := env.state[key] if len(v) < 8 { return 0 } return binary.BigEndian.Uint64(v[:8]) } // ── unit tests ──────────────────────────────────────────────────────────────── func TestValidate_ValidWASM(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) if err := v.Validate(ctx, wasmBytes); err != nil { t.Fatalf("Validate valid WASM: %v", err) } } func TestValidate_InvalidBytes(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) if err := v.Validate(ctx, []byte("not a wasm file")); err == nil { t.Fatal("expected error for invalid WASM bytes") } } func TestValidate_EmptyBytes(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) if err := v.Validate(ctx, []byte{}); err == nil { t.Fatal("expected error for empty bytes") } } func TestCall_UnknownMethod(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("caller1") _, err := v.Call(ctx, "test-id", wasmBytes, "nonexistent", nil, 100_000, env) if err == nil { t.Fatal("expected error for unknown method") } if !errors.Is(err, blockchain.ErrTxFailed) { t.Fatalf("expected ErrTxFailed, got: %v", err) } } func TestCall_GasExhausted(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("caller1") // Gas limit of 1 unit is far too low for any real work. _, err := v.Call(ctx, "test-id", wasmBytes, "increment", nil, 1, env) if err == nil { t.Fatal("expected ErrOutOfGas") } if !errors.Is(err, ErrOutOfGas) { t.Fatalf("expected ErrOutOfGas, got: %v", err) } if !errors.Is(err, blockchain.ErrTxFailed) { t.Fatalf("ErrOutOfGas must wrap ErrTxFailed, got: %v", err) } } // ── integration tests (counter contract) ───────────────────────────────────── func TestCounter_Increment(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("alice") for i := 1; i <= 3; i++ { gasUsed, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, env) if err != nil { t.Fatalf("increment %d: %v", i, err) } if gasUsed == 0 { t.Errorf("increment %d: expected gas > 0", i) } got := readU64State(env, "counter") if got != uint64(i) { t.Errorf("after increment %d: counter=%d, want %d", i, got, i) } } } func TestCounter_Get(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("alice") // Increment twice then call get. for i := 0; i < 2; i++ { if _, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, env); err != nil { t.Fatalf("increment: %v", err) } } if _, err := v.Call(ctx, "ctr", wasmBytes, "get", nil, 1_000_000, env); err != nil { t.Fatalf("get: %v", err) } // get logs "get called" logged := false for _, l := range env.logs { if l == "get called" { logged = true } } if !logged { t.Error("expected 'get called' in logs") } } func TestCounter_Reset_AuthorizedOwner(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("alice") // Increment 5 times. for i := 0; i < 5; i++ { if _, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, env); err != nil { t.Fatalf("increment: %v", err) } } if got := readU64State(env, "counter"); got != 5 { t.Fatalf("before reset: counter=%d, want 5", got) } // First reset — alice becomes owner. if _, err := v.Call(ctx, "ctr", wasmBytes, "reset", nil, 1_000_000, env); err != nil { t.Fatalf("reset (owner set): %v", err) } if got := readU64State(env, "counter"); got != 0 { t.Fatalf("after reset: counter=%d, want 0", got) } // Increment again then reset again (alice is owner). for i := 0; i < 3; i++ { if _, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, env); err != nil { t.Fatalf("increment: %v", err) } } if _, err := v.Call(ctx, "ctr", wasmBytes, "reset", nil, 1_000_000, env); err != nil { t.Fatalf("reset (owner confirmed): %v", err) } if got := readU64State(env, "counter"); got != 0 { t.Fatalf("second reset: counter=%d, want 0", got) } } func TestCounter_Reset_UnauthorizedRejected(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) envAlice := newMockEnv("alice") // Alice increments and performs first reset (sets herself as owner). if _, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, envAlice); err != nil { t.Fatalf("increment: %v", err) } if _, err := v.Call(ctx, "ctr", wasmBytes, "reset", nil, 1_000_000, envAlice); err != nil { t.Fatalf("reset (set owner): %v", err) } // Bob tries to reset using the same state but with his caller ID. // We simulate this by setting the owner in bob's env to alice's value. envBob := newMockEnv("bob") envBob.state = envAlice.state // shared state (bob sees alice as owner) // Bob increments so counter > 0. if _, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, envBob); err != nil { t.Fatalf("bob increment: %v", err) } counterBefore := readU64State(envBob, "counter") // Bob tries reset — should be rejected (logs "unauthorized"). if _, err := v.Call(ctx, "ctr", wasmBytes, "reset", nil, 1_000_000, envBob); err != nil { t.Fatalf("bob reset call error: %v", err) } // Counter should not be 0. if got := readU64State(envBob, "counter"); got == 0 { t.Errorf("bob reset succeeded (counter=%d), expected it to be rejected (was %d)", got, counterBefore) } // Should have logged "unauthorized". logged := false for _, l := range envBob.logs { if l == "unauthorized" { logged = true } } if !logged { t.Error("expected 'unauthorized' log when bob tries to reset") } } func TestCounter_GasReturned(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasmBytes := counterWASM(t) env := newMockEnv("alice") gasUsed, err := v.Call(ctx, "ctr", wasmBytes, "increment", nil, 1_000_000, env) if err != nil { t.Fatalf("increment: %v", err) } if gasUsed == 0 || gasUsed >= 1_000_000 { t.Errorf("unexpected gas: %d", gasUsed) } t.Logf("increment gasUsed=%d", gasUsed) } func TestABI_Validate(t *testing.T) { a, err := ParseABI(`{"methods":[{"name":"increment","args":[]},{"name":"get","args":[]},{"name":"reset","args":[]}]}`) if err != nil { t.Fatalf("ParseABI: %v", err) } if !a.HasMethod("increment") { t.Error("expected HasMethod(increment)") } if a.HasMethod("nonexistent") { t.Error("unexpected HasMethod(nonexistent)") } if err := a.Validate("increment", nil); err != nil { t.Errorf("Validate increment: %v", err) } if err := a.Validate("unknown", nil); err == nil { t.Error("expected error for unknown method") } } func TestABI_NamedArgs(t *testing.T) { a, err := ParseABI(`{"methods":[ {"name":"register","args":[{"name":"name","type":"string"}]}, {"name":"transfer","args":[{"name":"name","type":"string"},{"name":"new_owner","type":"string"}]} ]}`) if err != nil { t.Fatalf("ParseABI: %v", err) } if !a.HasMethod("register") { t.Error("expected HasMethod(register)") } // register expects 1 arg if err := a.Validate("register", []byte(`["alice"]`)); err != nil { t.Errorf("Validate register 1 arg: %v", err) } if err := a.Validate("register", []byte(`["alice","extra"]`)); err == nil { t.Error("expected error for too many args") } // transfer expects 2 args if err := a.Validate("transfer", []byte(`["alice","bob_pubkey"]`)); err != nil { t.Errorf("Validate transfer 2 args: %v", err) } // Inspect arg metadata if a.Methods[0].Args[0].Name != "name" { t.Errorf("arg name: want 'name', got %q", a.Methods[0].Args[0].Name) } if a.Methods[0].Args[0].Type != "string" { t.Errorf("arg type: want 'string', got %q", a.Methods[0].Args[0].Type) } } // ── name registry contract tests ────────────────────────────────────────────── func nameRegistryWASM(t *testing.T) []byte { t.Helper() data, err := os.ReadFile("../contracts/name_registry/name_registry.wasm") if err != nil { t.Fatalf("load name_registry.wasm: %v", err) } return data } func TestNameRegistry_Register(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) env := newMockEnv("alice_pubkey_hex") _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env) if err != nil { t.Fatalf("register: %v", err) } // State key "alice" should now contain the caller pubkey bytes. val := env.state["alice"] if string(val) != "alice_pubkey_hex" { t.Errorf("state[alice] = %q, want %q", val, "alice_pubkey_hex") } // Should have logged something containing "registered" if len(env.logs) == 0 || !strings.Contains(env.logs[len(env.logs)-1], "registered") { t.Errorf("expected last log 'registered', got %v", env.logs) } } func TestNameRegistry_NameTaken(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) env := newMockEnv("alice_pubkey_hex") // First registration succeeds. if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env); err != nil { t.Fatalf("first register: %v", err) } // Second registration by a different caller should log "name taken". env2 := newMockEnv("bob_pubkey_hex") env2.state = env.state // shared state logsBefore := len(env2.logs) if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env2); err != nil { t.Fatalf("second register: %v", err) } found := false for _, l := range env2.logs[logsBefore:] { if strings.Contains(l, "name taken") { found = true } } if !found { t.Errorf("expected 'name taken' log, got %v", env2.logs) } // Owner should still be alice. if string(env.state["alice"]) != "alice_pubkey_hex" { t.Error("owner changed unexpectedly") } } func TestNameRegistry_Resolve(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) env := newMockEnv("alice_pubkey_hex") if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env); err != nil { t.Fatalf("register: %v", err) } env.logs = nil if _, err := v.Call(ctx, "reg", wasm, "resolve", []byte(`["alice"]`), 1_000_000, env); err != nil { t.Fatalf("resolve: %v", err) } // Should log the owner pubkey (verbose: "owner: alice_pubkey_hex"). if len(env.logs) == 0 || !strings.Contains(env.logs[0], "alice_pubkey_hex") { t.Errorf("resolve logged %v, want something containing alice_pubkey_hex", env.logs) } // Resolve unknown name logs "not found". env.logs = nil if _, err := v.Call(ctx, "reg", wasm, "resolve", []byte(`["unknown"]`), 1_000_000, env); err != nil { t.Fatalf("resolve unknown: %v", err) } if len(env.logs) == 0 || !strings.Contains(env.logs[0], "not found") { t.Errorf("expected 'not found', got %v", env.logs) } } func TestNameRegistry_Transfer(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) envAlice := newMockEnv("alice_pubkey_hex") if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, envAlice); err != nil { t.Fatalf("register: %v", err) } // Alice transfers "alice" to bob. if _, err := v.Call(ctx, "reg", wasm, "transfer", []byte(`["alice","bob_pubkey_hex"]`), 1_000_000, envAlice); err != nil { t.Fatalf("transfer: %v", err) } if string(envAlice.state["alice"]) != "bob_pubkey_hex" { t.Errorf("state[alice] = %q, want bob_pubkey_hex", envAlice.state["alice"]) } lastLog := envAlice.logs[len(envAlice.logs)-1] if !strings.Contains(lastLog, "transferred") { t.Errorf("expected 'transferred' log, got %q", lastLog) } } func TestNameRegistry_Transfer_Unauthorized(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) envAlice := newMockEnv("alice_pubkey_hex") if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, envAlice); err != nil { t.Fatalf("register: %v", err) } // Bob tries to transfer alice's name. envBob := newMockEnv("bob_pubkey_hex") envBob.state = envAlice.state if _, err := v.Call(ctx, "reg", wasm, "transfer", []byte(`["alice","bob_pubkey_hex"]`), 1_000_000, envBob); err != nil { t.Fatalf("transfer call: %v", err) } // Owner should still be alice. if string(envBob.state["alice"]) != "alice_pubkey_hex" { t.Errorf("unauthorized transfer succeeded, state = %q", envBob.state["alice"]) } found := false for _, l := range envBob.logs { if strings.Contains(l, "unauthorized") { found = true } } if !found { t.Errorf("expected 'unauthorized' log, got %v", envBob.logs) } } func TestNameRegistry_Release(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) env := newMockEnv("alice_pubkey_hex") if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env); err != nil { t.Fatalf("register: %v", err) } if _, err := v.Call(ctx, "reg", wasm, "release", []byte(`["alice"]`), 1_000_000, env); err != nil { t.Fatalf("release: %v", err) } // State should now be empty (released). val := env.state["alice"] if len(val) > 0 { t.Errorf("after release state[alice] = %q, want empty", val) } lastLog := env.logs[len(env.logs)-1] if !strings.Contains(lastLog, "released") { t.Errorf("expected 'released' log, got %q", lastLog) } // After release, anyone can re-register. env2 := newMockEnv("charlie_pubkey_hex") env2.state = env.state if _, err := v.Call(ctx, "reg", wasm, "register", []byte(`["alice"]`), 1_000_000, env2); err != nil { t.Fatalf("re-register after release: %v", err) } if string(env2.state["alice"]) != "charlie_pubkey_hex" { t.Errorf("re-register: state[alice] = %q, want charlie_pubkey_hex", env2.state["alice"]) } } // ── Phase 9: instruction-level gas metering ─────────────────────────────────── // infiniteLoopWASM is a hand-encoded WASM module that exports one function, // "loop_forever", which contains an unconditional infinite loop. // The Instrument function must inject gas_tick so the loop is terminated. // // WAT equivalent: // // (module // (func (export "loop_forever") // (loop $L (br $L)) // ) // ) var infiniteLoopWASM = []byte{ // magic + version 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, // type section: 1 type — () → () 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, // function section: 1 function using type 0 0x03, 0x02, 0x01, 0x00, // export section: export "loop_forever" as function 0 0x07, 0x10, 0x01, 0x0C, 0x6C, 0x6F, 0x6F, 0x70, 0x5F, 0x66, 0x6F, 0x72, 0x65, 0x76, 0x65, 0x72, // "loop_forever" 0x00, 0x00, // code section: 1 entry — body: loop void; br 0; end; end 0x0A, 0x09, 0x01, 0x07, 0x00, 0x03, 0x40, // loop void 0x0C, 0x00, // br 0 0x0B, // end (loop) 0x0B, // end (function) } // TestInstrument_InfiniteLoop verifies that Instrument succeeds and produces // valid WASM for a module containing an infinite loop. func TestInstrument_InfiniteLoop(t *testing.T) { instrumented, err := Instrument(infiniteLoopWASM) if err != nil { t.Fatalf("Instrument: %v", err) } if len(instrumented) <= len(infiniteLoopWASM) { t.Errorf("instrumented binary (%d B) not larger than original (%d B)", len(instrumented), len(infiniteLoopWASM)) } // Must still be valid WASM. ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) if err := v.Validate(ctx, instrumented); err != nil { t.Fatalf("Validate instrumented: %v", err) } } // TestInstrument_Idempotent verifies that instrumenting an already-instrumented // binary is a no-op (returns identical bytes). func TestInstrument_Idempotent(t *testing.T) { once, err := Instrument(infiniteLoopWASM) if err != nil { t.Fatalf("first Instrument: %v", err) } twice, err := Instrument(once) if err != nil { t.Fatalf("second Instrument: %v", err) } if !bytes.Equal(once, twice) { t.Error("second instrumentation changed the binary — not idempotent") } } // TestInfiniteLoop_TrappedByGas verifies that an infinite loop is terminated // when the gas budget is exhausted. func TestInfiniteLoop_TrappedByGas(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) env := newMockEnv("alice") _, err := v.Call(ctx, "inf-loop", infiniteLoopWASM, "loop_forever", nil, 10_000, env) if err == nil { t.Fatal("expected error from infinite loop — should have been trapped by gas") } if !errors.Is(err, ErrOutOfGas) { t.Fatalf("expected ErrOutOfGas, got: %v", err) } if !errors.Is(err, blockchain.ErrTxFailed) { t.Fatalf("ErrOutOfGas must wrap ErrTxFailed, got: %v", err) } } // TestNameRegistry_GasIncludesLoopCost verifies that calling a contract method // that exercises a loop (bytes_equal in name_registry) produces a non-zero // gasUsed that is higher than the function-call minimum. func TestNameRegistry_GasIncludesLoopCost(t *testing.T) { ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) wasm := nameRegistryWASM(t) env := newMockEnv("alice_pubkey_hex") gasUsed, err := v.Call(ctx, "reg-gas", wasm, "register", []byte(`["alice"]`), 1_000_000, env) if err != nil { t.Fatalf("register: %v", err) } // The name-registry register function calls bytes_equal in a loop. // With loop-header metering, gas > just function-call overhead. if gasUsed == 0 { t.Error("gasUsed == 0, expected > 0") } t.Logf("name_registry.register gasUsed = %d", gasUsed) } // TestInstrument_CounterWASM verifies that the counter contract (no loops) // is instrumented without error and remains functionally identical. func TestInstrument_CounterWASM(t *testing.T) { original := counterWASM(t) instrumented, err := Instrument(original) if err != nil { t.Fatalf("Instrument counter: %v", err) } // Counter WASM has no loops — instrumented size may equal original. ctx := context.Background() v := NewVM(ctx) defer v.Close(ctx) if err := v.Validate(ctx, instrumented); err != nil { t.Fatalf("Validate instrumented counter: %v", err) } // Function still works after instrumentation. env := newMockEnv("alice") if _, err := v.Call(ctx, "ctr-instr", instrumented, "increment", nil, 1_000_000, env); err != nil { t.Fatalf("increment on instrumented counter: %v", err) } if readU64State(env, "counter") != 1 { t.Error("counter not incremented after instrumentation") } }