chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
This commit is contained in:
684
vm/vm_test.go
Normal file
684
vm/vm_test.go
Normal file
@@ -0,0 +1,684 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user