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

Binary file not shown.

View File

@@ -0,0 +1,102 @@
(module
;; Counter smart contract — methods: increment, get, reset
;; State key "counter" stores uint64 as 8-byte big-endian.
;; State key "owner" stores the deployer pub key set on first reset() call.
;; ── imports ──────────────────────────────────────────────────────────────
(import "env" "put_u64"
(func $put_u64 (param i32 i32 i64)))
(import "env" "get_u64"
(func $get_u64 (param i32 i32) (result i64)))
(import "env" "get_caller"
(func $get_caller (param i32 i32) (result i32)))
(import "env" "get_state"
(func $get_state (param i32 i32 i32 i32) (result i32)))
(import "env" "set_state"
(func $set_state (param i32 i32 i32 i32)))
(import "env" "log"
(func $log (param i32 i32)))
;; ── memory ───────────────────────────────────────────────────────────────
;; Memory layout:
;; offset 0 : "counter" (7 bytes) — state key for the counter value
;; offset 16 : "owner" (5 bytes) — state key for the owner pub key
;; offset 32 : caller buffer (128 bytes)
;; offset 160 : owner buffer (128 bytes)
;; offset 288 : log messages
(memory (export "memory") 1)
(data (i32.const 0) "counter")
(data (i32.const 16) "owner")
(data (i32.const 288) "incremented")
(data (i32.const 300) "get called")
(data (i32.const 310) "reset")
(data (i32.const 316) "unauthorized")
(data (i32.const 329) "reset ok")
;; ── increment() ──────────────────────────────────────────────────────────
(func (export "increment")
(local $val i64)
(local.set $val (call $get_u64 (i32.const 0) (i32.const 7)))
(local.set $val (i64.add (local.get $val) (i64.const 1)))
(call $put_u64 (i32.const 0) (i32.const 7) (local.get $val))
(call $log (i32.const 288) (i32.const 11))
)
;; ── get() ─────────────────────────────────────────────────────────────────
(func (export "get")
(call $log (i32.const 300) (i32.const 10))
)
;; ── reset() ───────────────────────────────────────────────────────────────
;; Resets counter to 0. Only callable by the deployer (first caller sets ownership).
(func (export "reset")
(local $callerLen i32)
(local $ownerLen i32)
(local $i i32)
(local $same i32)
(local.set $callerLen (call $get_caller (i32.const 32) (i32.const 128)))
(local.set $ownerLen
(call $get_state (i32.const 16) (i32.const 5) (i32.const 160) (i32.const 128)))
;; No owner yet — first caller becomes owner and resets to 0
(if (i32.eqz (local.get $ownerLen))
(then
(call $set_state (i32.const 16) (i32.const 5) (i32.const 32) (local.get $callerLen))
(call $put_u64 (i32.const 0) (i32.const 7) (i64.const 0))
(call $log (i32.const 329) (i32.const 8))
return
)
)
;; Length mismatch → unauthorized
(if (i32.ne (local.get $callerLen) (local.get $ownerLen))
(then (call $log (i32.const 316) (i32.const 12)) return)
)
;; Byte-by-byte comparison
(local.set $same (i32.const 1))
(local.set $i (i32.const 0))
(block $break
(loop $loop
(br_if $break (i32.ge_u (local.get $i) (local.get $callerLen)))
(if (i32.ne
(i32.load8_u (i32.add (i32.const 32) (local.get $i)))
(i32.load8_u (i32.add (i32.const 160) (local.get $i))))
(then (local.set $same (i32.const 0)) (br $break))
)
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $loop)
)
)
(if (i32.eqz (local.get $same))
(then (call $log (i32.const 316) (i32.const 12)) return)
)
;; Authorized — reset
(call $put_u64 (i32.const 0) (i32.const 7) (i64.const 0))
(call $log (i32.const 329) (i32.const 8))
)
)

View File

@@ -0,0 +1 @@
{"methods":[{"name":"increment","args":[]},{"name":"get","args":[]},{"name":"reset","args":[]}]}

View File

@@ -0,0 +1,331 @@
// gen generates contracts/counter/counter.wasm — binary WASM for the counter contract.
// Run from the repo root: go run ./contracts/counter/gen/
//
// Contract methods exported: increment, get, reset
// Host imports from "env": put_u64, get_u64, log, get_caller, get_state, set_state
package main
import (
"fmt"
"os"
)
// ── LEB128 ───────────────────────────────────────────────────────────────────
func u(v uint64) []byte {
var b []byte
for {
bt := byte(v & 0x7f)
v >>= 7
if v != 0 {
bt |= 0x80
}
b = append(b, bt)
if v == 0 {
return b
}
}
}
func s(v int64) []byte {
var b []byte
for {
bt := byte(v & 0x7f)
v >>= 7
sign := (bt & 0x40) != 0
if (v == 0 && !sign) || (v == -1 && sign) {
return append(b, bt)
}
b = append(b, bt|0x80)
}
}
// ── Builders ──────────────────────────────────────────────────────────────────
func cat(slices ...[]byte) []byte {
var out []byte
for _, s := range slices {
out = append(out, s...)
}
return out
}
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
func section(id byte, content []byte) []byte {
return cat([]byte{id}, u(uint64(len(content))), content)
}
// vec encodes a vector: count followed by concatenated items.
func vec(items ...[]byte) []byte {
out := u(uint64(len(items)))
for _, it := range items {
out = append(out, it...)
}
return out
}
// functype encodes a WASM function type (0x60 prefix).
func functype(params, results []byte) []byte {
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
}
// importFunc encodes a function import entry.
func importFunc(mod, name string, typeIdx uint32) []byte {
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
}
// exportEntry encodes an export entry.
func exportEntry(name string, kind byte, idx uint32) []byte {
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
}
// dataSegment encodes an active data segment for memory 0.
func dataSegment(offset int32, data []byte) []byte {
return cat(
[]byte{0x00}, // active segment, implicit mem 0
[]byte{0x41}, s(int64(offset)), []byte{0x0B}, // i32.const offset; end
u(uint64(len(data))), data,
)
}
// funcBody encodes one function body: localDecls + instructions + end.
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
inner := cat(localDecls)
for _, ins := range instrs {
inner = append(inner, ins...)
}
inner = append(inner, 0x0B) // end
return cat(u(uint64(len(inner))), inner)
}
// noLocals is an empty local decl list.
var noLocals = u(0)
// localDecl encodes n locals of a given type.
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
func withLocals(decls ...[]byte) []byte {
return cat(u(uint64(len(decls))), cat(decls...))
}
// ── Instructions ──────────────────────────────────────────────────────────────
const (
tI32 byte = 0x7F
tI64 byte = 0x7E
)
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
func ic64(v int64) []byte { return cat([]byte{0x42}, s(v)) }
func block_() []byte { return []byte{0x02, 0x40} }
func loop_() []byte { return []byte{0x03, 0x40} }
func if_() []byte { return []byte{0x04, 0x40} }
func end_() []byte { return []byte{0x0B} }
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
func return_() []byte { return []byte{0x0F} }
func i32Eqz() []byte { return []byte{0x45} }
func i32Ne() []byte { return []byte{0x47} }
func i32GeU() []byte { return []byte{0x4F} }
func i32Add() []byte { return []byte{0x6A} }
func i64Add() []byte { return []byte{0x7C} }
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} } // align=0, offset=0
// ── Memory layout constants ───────────────────────────────────────────────────
const (
offCounter = 0x00 // "counter" (7 bytes)
offOwner = 0x10 // "owner" (5 bytes)
offIncMsg = 0x20 // "incremented" (11 bytes)
offGetMsg = 0x30 // "get called" (10 bytes)
offResetOk = 0x40 // "reset ok" (8 bytes)
offUnauth = 0x50 // "unauthorized" (12 bytes)
offCallerBuf = 0x60 // caller buf (128 bytes)
offOwnerBuf = 0xE0 // owner buf (128 bytes)
)
// Import function indices
const (
fnPutU64 = 0 // put_u64(keyPtr, keyLen i32, val i64)
fnGetU64 = 1 // get_u64(keyPtr, keyLen i32) → i64
fnLog = 2 // log(msgPtr, msgLen i32)
fnGetCaller = 3 // get_caller(bufPtr, bufLen i32) → i32
fnGetState = 4 // get_state(kPtr,kLen,dPtr,dLen i32) → i32
fnSetState = 5 // set_state(kPtr,kLen,vPtr,vLen i32)
)
// Local function indices (imports are 0-5, locals start at 6)
const (
fnIncrement = 6
fnGet = 7
fnReset = 8
)
func main() {
// ── Type section ─────────────────────────────────────────────────────────
// Type 0: (i32,i32,i64)→() put_u64
// Type 1: (i32,i32)→(i64) get_u64
// Type 2: (i32,i32)→() log
// Type 3: (i32,i32)→(i32) get_caller
// Type 4: (i32,i32,i32,i32)→(i32) get_state
// Type 5: (i32,i32,i32,i32)→() set_state
// Type 6: ()→() increment, get, reset
typeSection := section(0x01, vec(
functype([]byte{tI32, tI32, tI64}, []byte{}), // 0
functype([]byte{tI32, tI32}, []byte{tI64}), // 1
functype([]byte{tI32, tI32}, []byte{}), // 2
functype([]byte{tI32, tI32}, []byte{tI32}), // 3
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 4
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 5
functype([]byte{}, []byte{}), // 6
))
// ── Import section ────────────────────────────────────────────────────────
importSection := section(0x02, vec(
importFunc("env", "put_u64", fnPutU64),
importFunc("env", "get_u64", fnGetU64),
importFunc("env", "log", fnLog),
importFunc("env", "get_caller", fnGetCaller),
importFunc("env", "get_state", fnGetState),
importFunc("env", "set_state", fnSetState),
))
// ── Function section: 3 local functions, all type 6 ──────────────────────
functionSection := section(0x03, vec(u(6), u(6), u(6)))
// ── Memory section: 1 page (64 KiB) ──────────────────────────────────────
// limits type 0x00 = min only; type 0x01 = min+max
memorySection := section(0x05, vec(cat([]byte{0x00}, u(1)))) // min=1, no max
// ── Export section ────────────────────────────────────────────────────────
exportSection := section(0x07, vec(
exportEntry("memory", 0x02, 0),
exportEntry("increment", 0x00, fnIncrement),
exportEntry("get", 0x00, fnGet),
exportEntry("reset", 0x00, fnReset),
))
// ── Data section ──────────────────────────────────────────────────────────
dataSection := section(0x0B, cat(
u(6), // 6 segments
dataSegment(offCounter, []byte("counter")),
dataSegment(offOwner, []byte("owner")),
dataSegment(offIncMsg, []byte("incremented")),
dataSegment(offGetMsg, []byte("get called")),
dataSegment(offResetOk, []byte("reset ok")),
dataSegment(offUnauth, []byte("unauthorized")),
))
// ── Code section ─────────────────────────────────────────────────────────
// increment():
// local $val i64
// $val = get_u64("counter")
// $val++
// put_u64("counter", $val)
// log("incremented")
incrementBody := funcBody(
withLocals(localDecl(1, tI64)),
ic32(offCounter), ic32(7), call(fnGetU64), lset(0),
lget(0), ic64(1), i64Add(), lset(0),
ic32(offCounter), ic32(7), lget(0), call(fnPutU64),
ic32(offIncMsg), ic32(11), call(fnLog),
)
// get():
// log("get called")
getBody := funcBody(
noLocals,
ic32(offGetMsg), ic32(10), call(fnLog),
)
// reset():
// locals: callerLen(0), ownerLen(1), i(2), same(3) — all i32
// callerLen = get_caller(callerBuf, 128)
// ownerLen = get_state("owner", ownerBuf, 128)
// if ownerLen == 0:
// set_state("owner", callerBuf[:callerLen])
// put_u64("counter", 0)
// log("reset ok")
// return
// if callerLen != ownerLen: log unauthorized; return
// same = 1; i = 0
// block:
// loop:
// if i >= callerLen: br 1 (exit block)
// if callerBuf[i] != ownerBuf[i]: same=0; br 1
// i++; continue loop
// if !same: log unauthorized; return
// put_u64("counter", 0); log("reset ok")
resetBody := funcBody(
withLocals(localDecl(4, tI32)),
// callerLen = get_caller(callerBuf, 128)
ic32(offCallerBuf), ic32(128), call(fnGetCaller), lset(0),
// ownerLen = get_state("owner", 5, ownerBuf, 128)
ic32(offOwner), ic32(5), ic32(offOwnerBuf), ic32(128), call(fnGetState), lset(1),
// if ownerLen == 0:
lget(1), i32Eqz(), if_(),
ic32(offOwner), ic32(5), ic32(offCallerBuf), lget(0), call(fnSetState),
ic32(offCounter), ic32(7), ic64(0), call(fnPutU64),
ic32(offResetOk), ic32(8), call(fnLog),
return_(),
end_(),
// if callerLen != ownerLen: unauthorized
lget(0), lget(1), i32Ne(), if_(),
ic32(offUnauth), ic32(12), call(fnLog),
return_(),
end_(),
// same = 1; i = 0
ic32(1), lset(3),
ic32(0), lset(2),
// block $break
block_(),
loop_(),
lget(2), lget(0), i32GeU(), brIf_(1), // i >= callerLen → break
// load callerBuf[i]
ic32(offCallerBuf), lget(2), i32Add(), i32Load8U(),
// load ownerBuf[i]
ic32(offOwnerBuf), lget(2), i32Add(), i32Load8U(),
i32Ne(), if_(),
ic32(0), lset(3),
br_(2), // break out of block
end_(),
lget(2), ic32(1), i32Add(), lset(2),
br_(0), // continue loop
end_(),
end_(),
// if !same: unauthorized
lget(3), i32Eqz(), if_(),
ic32(offUnauth), ic32(12), call(fnLog),
return_(),
end_(),
// authorized
ic32(offCounter), ic32(7), ic64(0), call(fnPutU64),
ic32(offResetOk), ic32(8), call(fnLog),
)
codeSection := section(0x0A, cat(u(3), incrementBody, getBody, resetBody))
// ── Assemble module ───────────────────────────────────────────────────────
module := cat(
[]byte{0x00, 0x61, 0x73, 0x6d}, // magic \0asm
[]byte{0x01, 0x00, 0x00, 0x00}, // version 1
typeSection,
importSection,
functionSection,
memorySection,
exportSection,
dataSection,
codeSection,
)
out := "contracts/counter/counter.wasm"
if err := os.WriteFile(out, module, 0644); err != nil {
fmt.Fprintln(os.Stderr, "write:", err)
os.Exit(1)
}
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
}

137
contracts/counter/main.go Normal file
View File

@@ -0,0 +1,137 @@
// Counter smart contract — compiles to WASM with GOOS=wasip1 GOARCH=wasm.
//
// Methods (exported via //go:export):
// - increment — adds 1 to the stored counter
// - get — logs the current value (readable via /api/contracts/{id}/state/counter)
// - reset — resets counter to 0; only the first caller (owner) is allowed
//
// Host imports from the "env" module (see vm/host.go):
// - put_u64(keyPtr, keyLen, val) — stores uint64 as 8-byte big-endian
// - get_u64(keyPtr, keyLen) uint64 — reads 8-byte big-endian uint64
// - get_caller(buf, bufLen) int32 — writes caller pub key hex into buf
// - get_state(kPtr,kLen,dPtr,dLen) int32 — reads raw state bytes
// - set_state(kPtr,kLen,vPtr,vLen) — writes raw state bytes
// - log(msgPtr, msgLen) — emits message to node log
//
//go:build wasip1
package main
import (
"unsafe"
)
// ── host function imports ─────────────────────────────────────────────────────
//go:wasmimport env put_u64
func hostPutU64(keyPtr unsafe.Pointer, keyLen int32, val uint64)
//go:wasmimport env get_u64
func hostGetU64(keyPtr unsafe.Pointer, keyLen int32) uint64
//go:wasmimport env get_caller
func hostGetCaller(buf unsafe.Pointer, bufLen int32) int32
//go:wasmimport env get_state
func hostGetState(keyPtr unsafe.Pointer, keyLen int32, dstPtr unsafe.Pointer, dstLen int32) int32
//go:wasmimport env set_state
func hostSetState(keyPtr unsafe.Pointer, keyLen int32, valPtr unsafe.Pointer, valLen int32)
//go:wasmimport env log
func hostLog(msgPtr unsafe.Pointer, msgLen int32)
// ── helpers ───────────────────────────────────────────────────────────────────
func logMsg(s string) {
if len(s) == 0 {
return
}
b := []byte(s)
hostLog(unsafe.Pointer(&b[0]), int32(len(b)))
}
func putU64(key string, val uint64) {
b := []byte(key)
hostPutU64(unsafe.Pointer(&b[0]), int32(len(b)), val)
}
func getU64(key string) uint64 {
b := []byte(key)
return hostGetU64(unsafe.Pointer(&b[0]), int32(len(b)))
}
func getState(key string, dst []byte) int32 {
kb := []byte(key)
return hostGetState(unsafe.Pointer(&kb[0]), int32(len(kb)),
unsafe.Pointer(&dst[0]), int32(len(dst)))
}
func setState(key string, val []byte) {
kb := []byte(key)
hostSetState(unsafe.Pointer(&kb[0]), int32(len(kb)),
unsafe.Pointer(&val[0]), int32(len(val)))
}
func getCaller() string {
buf := make([]byte, 128)
n := hostGetCaller(unsafe.Pointer(&buf[0]), int32(len(buf)))
if n <= 0 {
return ""
}
return string(buf[:n])
}
// ── contract state keys ───────────────────────────────────────────────────────
const (
keyCounter = "counter"
keyOwner = "owner"
)
// ── exported contract methods ─────────────────────────────────────────────────
//go:export increment
func increment() {
val := getU64(keyCounter)
val++
putU64(keyCounter, val)
logMsg("incremented")
}
//go:export get
func get() {
logMsg("get called")
}
//go:export reset
func reset() {
caller := getCaller()
if caller == "" {
logMsg("reset: no caller")
return
}
ownerBuf := make([]byte, 128)
ownerLen := getState(keyOwner, ownerBuf)
if ownerLen == 0 {
// No owner set yet — first caller becomes the owner.
setState(keyOwner, []byte(caller))
putU64(keyCounter, 0)
logMsg("reset ok (owner set)")
return
}
owner := string(ownerBuf[:ownerLen])
if caller != owner {
logMsg("reset: unauthorized")
return
}
putU64(keyCounter, 0)
logMsg("reset ok")
}
// main is required by the Go runtime for wasip1 programs.
func main() {}