Files
dchain/vm/vm_test.go
vsecoder 7e7393e4f8 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
2026-04-17 14:16:44 +03:00

685 lines
21 KiB
Go

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")
}
}