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

238
blockchain/native.go Normal file
View File

@@ -0,0 +1,238 @@
// Package blockchain — native (non-WASM) contract infrastructure.
//
// System contracts like `username_registry` are latency-sensitive and must
// never hang the chain. Running them through the WASM VM means:
//
// - 100× the CPU cost of equivalent Go code;
// - a bug in gas metering / opcode instrumentation can freeze AddBlock
// indefinitely (see the hangs that motivated this rewrite);
// - every node needs an identical wazero build — extra supply-chain risk.
//
// Native contracts are written as plain Go code against a narrow interface
// (NativeContract below). They share the same contract_id space, ABI, and
// explorer views as WASM contracts, so clients can't tell them apart — the
// dispatcher in applyTx just routes the call to Go instead of wazero when
// it sees a native contract_id.
//
// Authorship notes:
// - A native contract has full, direct access to the current BadgerDB txn
// and chain helpers via NativeContext. It MUST only read/write keys
// prefixed with `cstate:<contractID>:` or `clog:<contractID>:…` — same
// as WASM contracts see. This keeps on-chain state cleanly segregated
// so one day we can migrate a native contract back to WASM (or vice
// versa) without a storage migration.
// - A native contract MUST return deterministic errors. The dispatcher
// treats any returned error as `ErrTxFailed`-wrapped — fees stay
// debited, but state changes roll back with the enclosing Badger txn.
package blockchain
import (
"encoding/json"
"errors"
"fmt"
badger "github.com/dgraph-io/badger/v4"
)
// NativeContract is the Go-side counterpart of a WASM smart contract.
//
// Implementors are expected to be stateless (all state lives in BadgerDB
// under cstate:<ContractID>:…). An instance is created once per chain and
// reused across all calls.
type NativeContract interface {
// ID returns the deterministic contract ID used in CALL_CONTRACT txs.
// Must be stable across node restarts and identical on every node.
ID() string
// ABI returns a JSON document describing the contract's methods.
// Identical shape to the WASM contracts' *_abi.json files so the
// well-known endpoint and explorer can discover it uniformly.
ABI() string
// Call dispatches a method invocation. Returns the gas it wants to
// charge (will be multiplied by the current gas price). Returning an
// error aborts the tx; returning (0, nil) means free success.
Call(ctx *NativeContext, method string, argsJSON []byte) (gasUsed uint64, err error)
}
// NativeContext hands a native contract the minimum it needs to run,
// without exposing the full Chain type (which would tempt contracts to
// touch state they shouldn't).
type NativeContext struct {
Txn *badger.Txn
ContractID string
Caller string // hex Ed25519 pubkey of the tx sender
TxID string
BlockHeight uint64
// TxAmount is tx.Amount — the payment the caller attached to this call
// in µT. It is NOT auto-debited from the caller; the contract decides
// whether to collect it (via ctx.Debit), refund, or ignore. Exposing
// payment via tx.Amount (instead of an implicit debit inside the
// contract) makes contract costs visible in the explorer — a user can
// see exactly what a call charges by reading the tx envelope.
TxAmount uint64
// chain is kept unexported; contract code uses the helper methods below
// rather than reaching into Chain directly.
chain *Chain
}
// Balance returns the balance of the given pubkey in µT.
func (ctx *NativeContext) Balance(pubHex string) uint64 {
var bal uint64
item, err := ctx.Txn.Get([]byte(prefixBalance + pubHex))
if errors.Is(err, badger.ErrKeyNotFound) {
return 0
}
if err != nil {
return 0
}
_ = item.Value(func(val []byte) error {
return unmarshalUint64(val, &bal)
})
return bal
}
// Debit removes amt µT from pub's balance, or returns an error if insufficient.
func (ctx *NativeContext) Debit(pub string, amt uint64) error {
return ctx.chain.debitBalance(ctx.Txn, pub, amt)
}
// Credit adds amt µT to pub's balance.
func (ctx *NativeContext) Credit(pub string, amt uint64) error {
return ctx.chain.creditBalance(ctx.Txn, pub, amt)
}
// Get reads a contract-scoped state value. Returns nil if not set.
func (ctx *NativeContext) Get(key string) ([]byte, error) {
item, err := ctx.Txn.Get([]byte(prefixContractState + ctx.ContractID + ":" + key))
if errors.Is(err, badger.ErrKeyNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
var out []byte
err = item.Value(func(v []byte) error {
out = append([]byte(nil), v...)
return nil
})
return out, err
}
// Set writes a contract-scoped state value.
func (ctx *NativeContext) Set(key string, value []byte) error {
return ctx.Txn.Set([]byte(prefixContractState+ctx.ContractID+":"+key), value)
}
// Delete removes a contract-scoped state value.
func (ctx *NativeContext) Delete(key string) error {
return ctx.Txn.Delete([]byte(prefixContractState + ctx.ContractID + ":" + key))
}
// Log emits a contract log line for the explorer. Uses the same storage as
// WASM contracts' env.log() so the explorer renders them identically.
func (ctx *NativeContext) Log(msg string) error {
return ctx.chain.writeContractLog(ctx.Txn, ctx.ContractID, ctx.BlockHeight, ctx.TxID, msg)
}
// ─── Native contract registry ────────────────────────────────────────────────
// Native contracts are registered into the chain once during chain setup
// (typically right after `NewChain`). Lookups happen on every CALL_CONTRACT
// and DEPLOY_CONTRACT — they're hot path, so the registry is a plain map
// guarded by a RW mutex.
// RegisterNative associates a NativeContract with its ID on this chain.
// Panics if two contracts share an ID (clear programmer error).
// Must be called before AddBlock begins processing user transactions.
//
// Uses a DEDICATED mutex (c.nativeMu) rather than c.mu, because
// lookupNative is called from inside applyTx which runs under c.mu.Lock().
// sync.RWMutex is non-reentrant — reusing c.mu would deadlock.
func (c *Chain) RegisterNative(nc NativeContract) {
c.nativeMu.Lock()
defer c.nativeMu.Unlock()
if c.native == nil {
c.native = make(map[string]NativeContract)
}
if _, exists := c.native[nc.ID()]; exists {
panic(fmt.Sprintf("native contract %s registered twice", nc.ID()))
}
c.native[nc.ID()] = nc
}
// lookupNative returns the registered native contract for id, or nil.
// Hot path — called on every CALL_CONTRACT from applyTx. Safe to call while
// c.mu is held because we use a separate RWMutex here.
func (c *Chain) lookupNative(id string) NativeContract {
c.nativeMu.RLock()
defer c.nativeMu.RUnlock()
return c.native[id]
}
// NativeContracts returns a snapshot of every native contract registered on
// this chain. Used by the well-known endpoint so clients auto-discover
// system services without the user having to paste contract IDs.
func (c *Chain) NativeContracts() []NativeContract {
c.nativeMu.RLock()
defer c.nativeMu.RUnlock()
out := make([]NativeContract, 0, len(c.native))
for _, nc := range c.native {
out = append(out, nc)
}
return out
}
// writeContractLog is the shared log emitter for both WASM and native
// contracts. Keeping it here (on Chain) means we can change the log key
// layout in one place.
func (c *Chain) writeContractLog(txn *badger.Txn, contractID string, blockHeight uint64, txID, msg string) error {
// Best-effort: match the existing WASM log format so the explorer's
// renderer doesn't need to branch.
seq := c.nextContractLogSeq(txn, contractID, blockHeight)
entry := ContractLogEntry{
ContractID: contractID,
BlockHeight: blockHeight,
TxID: txID,
Seq: int(seq),
Message: msg,
}
val, err := json.Marshal(entry)
if err != nil {
return err
}
key := fmt.Sprintf("%s%s:%020d:%05d", prefixContractLog, contractID, blockHeight, seq)
return txn.Set([]byte(key), val)
}
// nextContractLogSeq returns the next sequence number for a (contract,block)
// pair by counting existing entries under the prefix.
func (c *Chain) nextContractLogSeq(txn *badger.Txn, contractID string, blockHeight uint64) uint32 {
prefix := []byte(fmt.Sprintf("%s%s:%020d:", prefixContractLog, contractID, blockHeight))
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Prefix = prefix
it := txn.NewIterator(opts)
defer it.Close()
var count uint32
for it.Rewind(); it.Valid(); it.Next() {
count++
}
return count
}
// ─── Small helpers used by native contracts ──────────────────────────────────
// Uint64 is a tiny helper for reading a uint64 stored as 8 big-endian bytes.
// (We deliberately don't use JSON for hot state keys.)
func unmarshalUint64(b []byte, dst *uint64) error {
if len(b) != 8 {
return fmt.Errorf("not a uint64")
}
*dst = uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])
return nil
}