Files
dchain/blockchain/native.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

239 lines
9.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}