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

199
node/api_contract.go Normal file
View File

@@ -0,0 +1,199 @@
package node
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"go-blockchain/blockchain"
)
// registerContractAPI mounts the contract API routes on mux.
//
// GET /api/contracts — list all deployed contracts
// GET /api/contracts/{contractID} — contract metadata (no WASM bytes)
// GET /api/contracts/{contractID}/state/{key} — raw state value, base64-encoded
// GET /api/contracts/{contractID}/logs — recent log entries (newest first)
func registerContractAPI(mux *http.ServeMux, q ExplorerQuery) {
// Exact match for list endpoint (no trailing slash)
mux.HandleFunc("/api/contracts", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
jsonErr(w, errorf("method not allowed"), 405)
return
}
if q.GetContracts == nil {
jsonErr(w, errorf("contract queries not available"), 503)
return
}
contracts, err := q.GetContracts()
if err != nil {
jsonErr(w, err, 500)
return
}
if contracts == nil {
contracts = []blockchain.ContractRecord{}
}
jsonOK(w, map[string]any{
"count": len(contracts),
"contracts": contracts,
})
})
mux.HandleFunc("/api/contracts/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
jsonErr(w, errorf("method not allowed"), 405)
return
}
// Path segments after /api/contracts/:
// "<id>" → contract info
// "<id>/state/<k>" → state value
// "<id>/logs" → log entries
path := strings.TrimPrefix(r.URL.Path, "/api/contracts/")
path = strings.Trim(path, "/")
switch {
case strings.Contains(path, "/state/"):
parts := strings.SplitN(path, "/state/", 2)
contractID := parts[0]
if contractID == "" {
jsonErr(w, errorf("contract_id required"), 400)
return
}
handleContractState(w, q, contractID, parts[1])
case strings.HasSuffix(path, "/logs"):
contractID := strings.TrimSuffix(path, "/logs")
if contractID == "" {
jsonErr(w, errorf("contract_id required"), 400)
return
}
handleContractLogs(w, r, q, contractID)
default:
contractID := path
if contractID == "" {
jsonErr(w, errorf("contract_id required"), 400)
return
}
handleContractInfo(w, q, contractID)
}
})
}
func handleContractInfo(w http.ResponseWriter, q ExplorerQuery, contractID string) {
// Check native contracts first — they aren't stored as WASM ContractRecord
// in BadgerDB but are valid targets for CALL_CONTRACT and have an ABI.
if q.NativeContracts != nil {
for _, nc := range q.NativeContracts() {
if nc.ContractID == contractID {
jsonOK(w, map[string]any{
"contract_id": nc.ContractID,
"deployer_pub": "",
"deployed_at": uint64(0), // native contracts exist from genesis
"abi_json": nc.ABIJson,
"wasm_size": 0,
"native": true,
})
return
}
}
}
if q.GetContract == nil {
jsonErr(w, errorf("contract queries not available"), 503)
return
}
rec, err := q.GetContract(contractID)
if err != nil {
jsonErr(w, err, 500)
return
}
if rec == nil {
jsonErr(w, errorf("contract %s not found", contractID), 404)
return
}
// Omit raw WASM bytes from the API response; expose only metadata.
jsonOK(w, map[string]any{
"contract_id": rec.ContractID,
"deployer_pub": rec.DeployerPub,
"deployed_at": rec.DeployedAt,
"abi_json": rec.ABIJson,
"wasm_size": len(rec.WASMBytes),
"native": false,
})
}
func handleContractState(w http.ResponseWriter, q ExplorerQuery, contractID, key string) {
if q.GetContractState == nil {
jsonErr(w, errorf("contract state queries not available"), 503)
return
}
val, err := q.GetContractState(contractID, key)
if err != nil {
jsonErr(w, err, 500)
return
}
if val == nil {
jsonOK(w, map[string]any{
"contract_id": contractID,
"key": key,
"value_b64": nil,
"value_hex": nil,
})
return
}
jsonOK(w, map[string]any{
"contract_id": contractID,
"key": key,
"value_b64": base64.StdEncoding.EncodeToString(val),
"value_hex": hexEncode(val),
"value_u64": decodeU64(val), // convenience: big-endian uint64 if len==8
})
}
func handleContractLogs(w http.ResponseWriter, r *http.Request, q ExplorerQuery, contractID string) {
if q.GetContractLogs == nil {
jsonErr(w, errorf("contract log queries not available"), 503)
return
}
limit := 50
if s := r.URL.Query().Get("limit"); s != "" {
if n, err := strconv.Atoi(s); err == nil && n > 0 {
limit = n
}
}
entries, err := q.GetContractLogs(contractID, limit)
if err != nil {
jsonErr(w, err, 500)
return
}
if entries == nil {
entries = []blockchain.ContractLogEntry{}
}
jsonOK(w, map[string]any{
"contract_id": contractID,
"count": len(entries),
"logs": entries,
})
}
// errorf is a helper to create a formatted error.
func errorf(format string, args ...any) error {
if len(args) == 0 {
return fmt.Errorf("%s", format)
}
return fmt.Errorf(format, args...)
}
func hexEncode(b []byte) string { return hex.EncodeToString(b) }
func decodeU64(b []byte) *uint64 {
if len(b) != 8 {
return nil
}
v := binary.BigEndian.Uint64(b)
return &v
}