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:
238
blockchain/native.go
Normal file
238
blockchain/native.go
Normal 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user