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:
vsecoder
2026-04-17 14:16:44 +03:00
commit 7e7393e4f8
196 changed files with 55947 additions and 0 deletions

184
vm/vm.go Normal file
View File

@@ -0,0 +1,184 @@
// Package vm provides a WASM-based smart contract execution engine.
//
// The engine uses wazero (pure-Go, no CGO) in interpreter mode to guarantee
// deterministic execution across all platforms — a requirement for consensus.
//
// Contract lifecycle:
// 1. DEPLOY_CONTRACT tx → chain calls Validate() to compile-check the WASM,
// then stores the bytecode in BadgerDB.
// 2. CALL_CONTRACT tx → chain calls Call() with the stored WASM bytes,
// the method name, JSON args, and a gas limit.
//
// Gas model: each WASM function call costs gasPerCall (100) units.
// Gas cost in µT = gasUsed × blockchain.GasPrice.
package vm
import (
"context"
"fmt"
"log"
"sync"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"go-blockchain/blockchain"
)
// VM is a WASM execution engine. Create one per process; it is safe for
// concurrent use. Compiled modules are cached by contract ID.
type VM struct {
rt wazero.Runtime
mu sync.RWMutex
cache map[string]wazero.CompiledModule // contractID → compiled module
}
// NewVM creates a VM with an interpreter-mode wazero runtime.
// Interpreter mode guarantees identical behaviour on all OS/arch combos,
// which is required for all nodes to reach the same state.
//
// WASI preview 1 is pre-instantiated to support contracts compiled with
// TinyGo's wasip1 target (tinygo build -target wasip1). Contracts that do
// not import from wasi_snapshot_preview1 are unaffected.
func NewVM(ctx context.Context) *VM {
cfg := wazero.NewRuntimeConfigInterpreter()
cfg = cfg.WithDebugInfoEnabled(false)
// Enable cooperative cancellation: fn.Call(ctx) will return when ctx is
// cancelled, even if the contract is in an infinite loop. Without this,
// a buggy or malicious contract that dodges gas metering (e.g. a tight
// loop of opcodes not hooked by the gas listener) would hang the
// AddBlock goroutine forever, freezing the entire chain.
cfg = cfg.WithCloseOnContextDone(true)
// Attach call-level gas metering.
ctx = experimental.WithFunctionListenerFactory(ctx, gasListenerFactory{})
rt := wazero.NewRuntimeWithConfig(ctx, cfg)
// Instantiate WASI so TinyGo contracts can call proc_exit and basic I/O.
// The sandbox is fully isolated — no filesystem or network access.
wasi_snapshot_preview1.MustInstantiate(ctx, rt)
return &VM{
rt: rt,
cache: make(map[string]wazero.CompiledModule),
}
}
// Close releases the underlying wazero runtime.
func (v *VM) Close(ctx context.Context) error {
return v.rt.Close(ctx)
}
// Validate compiles the WASM bytes without executing them.
// Returns an error if the bytes are not valid WASM or import unknown symbols.
// Implements blockchain.ContractVM.
func (v *VM) Validate(ctx context.Context, wasmBytes []byte) error {
_, err := v.rt.CompileModule(ctx, wasmBytes)
if err != nil {
return fmt.Errorf("invalid WASM module: %w", err)
}
return nil
}
// Call executes method on the contract identified by contractID.
// wasmBytes is the compiled WASM (loaded from DB by the chain).
// Returns gas consumed. Returns ErrOutOfGas (wrapping ErrTxFailed) on exhaustion.
// Implements blockchain.ContractVM.
func (v *VM) Call(
ctx context.Context,
contractID string,
wasmBytes []byte,
method string,
argsJSON []byte,
gasLimit uint64,
env blockchain.VMHostEnv,
) (gasUsed uint64, err error) {
// Attach gas counter to context.
ctx, gc := withGasCounter(ctx, gasLimit)
ctx = experimental.WithFunctionListenerFactory(ctx, gasListenerFactory{})
// Compile (or retrieve from cache).
compiled, err := v.compiled(ctx, contractID, wasmBytes)
if err != nil {
return 0, fmt.Errorf("compile contract %s: %w", contractID, err)
}
// Instantiate the "env" host module for this call, wiring in typed args.
hostInst, err := registerHostModule(ctx, v.rt, env, argsJSON)
if err != nil {
return 0, fmt.Errorf("register host module: %w", err)
}
defer hostInst.Close(ctx)
// Instantiate the contract module.
modCfg := wazero.NewModuleConfig().
WithName(""). // anonymous — allows multiple concurrent instances
WithStartFunctions() // do not auto-call _start
mod, err := v.rt.InstantiateModule(ctx, compiled, modCfg)
if err != nil {
return gc.Used(), fmt.Errorf("instantiate contract: %w", err)
}
defer mod.Close(ctx)
// Look up the exported method.
fn := mod.ExportedFunction(method)
if fn == nil {
return gc.Used(), fmt.Errorf("%w: method %q not exported by contract %s",
blockchain.ErrTxFailed, method, contractID)
}
// Call. WASM functions called from Go pass args via the stack; our contracts
// use no parameters — all input/output goes through host state functions.
_, callErr := fn.Call(ctx)
gasUsed = gc.Used()
if callErr != nil {
log.Printf("[VM] contract %s.%s error: %v", contractID[:8], method, callErr)
if isOutOfGas(gc) {
return gasUsed, ErrOutOfGas
}
return gasUsed, fmt.Errorf("%w: %v", blockchain.ErrTxFailed, callErr)
}
if isOutOfGas(gc) {
return gasUsed, ErrOutOfGas
}
return gasUsed, nil
}
// compiled returns a cached compiled module, compiling it first if not cached.
// Before compiling, the WASM bytes are instrumented with loop-header gas_tick
// calls so that infinite loops are bounded by the gas limit.
func (v *VM) compiled(ctx context.Context, contractID string, wasmBytes []byte) (wazero.CompiledModule, error) {
v.mu.RLock()
cm, ok := v.cache[contractID]
v.mu.RUnlock()
if ok {
return cm, nil
}
v.mu.Lock()
defer v.mu.Unlock()
// Double-check after acquiring write lock.
if cm, ok = v.cache[contractID]; ok {
return cm, nil
}
// Instrument: inject gas_tick at loop headers.
// On instrumentation failure, fall back to the original bytes so that
// unusual WASM features do not prevent execution entirely.
instrumented, err := Instrument(wasmBytes)
if err != nil {
log.Printf("[VM] instrument contract %s: %v (using original bytes)", contractID[:min8(contractID)], err)
instrumented = wasmBytes
}
compiled, err := v.rt.CompileModule(ctx, instrumented)
if err != nil {
return nil, err
}
v.cache[contractID] = compiled
return compiled, nil
}
func min8(s string) int {
if len(s) < 8 {
return len(s)
}
return 8
}