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
302 lines
12 KiB
Plaintext
302 lines
12 KiB
Plaintext
(module
|
|
;; Name Registry smart contract
|
|
;;
|
|
;; Maps human-readable names → owner public keys on-chain.
|
|
;; Each name can only be registered once; only the current owner can
|
|
;; transfer or release it.
|
|
;;
|
|
;; Methods (all void, no WASM return values):
|
|
;; register(name string) — claim a name for the caller
|
|
;; resolve(name string) — log the current owner pubkey
|
|
;; transfer(name string, new_owner string) — give name to another pubkey
|
|
;; release(name string) — delete name registration
|
|
;;
|
|
;; State keys: the raw name bytes → owner pubkey bytes.
|
|
;; All args come in via env.get_arg_str / env.get_arg_u64.
|
|
|
|
;; ── imports ──────────────────────────────────────────────────────────────
|
|
(import "env" "get_arg_str"
|
|
(func $get_arg_str (param i32 i32 i32) (result i32)))
|
|
(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 ───────────────────────────────────────────────────────────────
|
|
;; Offset Size Purpose
|
|
;; 0x000 64 arg[0] buffer — name (max 64 bytes)
|
|
;; 0x040 128 arg[1] buffer — new_owner pubkey (max 128 bytes)
|
|
;; 0x0C0 128 caller pubkey buffer
|
|
;; 0x140 128 state-read buffer (existing owner)
|
|
;; 0x200 ~128 verbose log prefix strings
|
|
;; 0x300 256 scratch buffer — build "prefix: name" log messages
|
|
(memory (export "memory") 1)
|
|
|
|
;; ── verbose log prefix strings ───────────────────────────────────────────
|
|
;; Each entry is a human-readable prefix ending with ": " so that the
|
|
;; log message becomes "prefix: <argument>" — readable in the explorer.
|
|
;;
|
|
;; "registered: " 12 bytes @ 0x200
|
|
;; "name taken: " 12 bytes @ 0x20C
|
|
;; "not found: " 11 bytes @ 0x218
|
|
;; "owner: " 7 bytes @ 0x224
|
|
;; "transferred: " 13 bytes @ 0x22C
|
|
;; "unauthorized: " 14 bytes @ 0x23A
|
|
;; "released: " 10 bytes @ 0x249
|
|
(data (i32.const 0x200) "registered: ")
|
|
(data (i32.const 0x20C) "name taken: ")
|
|
(data (i32.const 0x218) "not found: ")
|
|
(data (i32.const 0x224) "owner: ")
|
|
(data (i32.const 0x22C) "transferred: ")
|
|
(data (i32.const 0x23A) "unauthorized: ")
|
|
(data (i32.const 0x249) "released: ")
|
|
|
|
;; ── helpers ───────────────────────────────────────────────────────────────
|
|
|
|
;; $memcpy: copy len bytes from src to dst
|
|
(func $memcpy (param $dst i32) (param $src i32) (param $len i32)
|
|
(local $i i32)
|
|
(local.set $i (i32.const 0))
|
|
(block $break
|
|
(loop $loop
|
|
(br_if $break (i32.ge_u (local.get $i) (local.get $len)))
|
|
(i32.store8
|
|
(i32.add (local.get $dst) (local.get $i))
|
|
(i32.load8_u (i32.add (local.get $src) (local.get $i))))
|
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
(br $loop)
|
|
)
|
|
)
|
|
)
|
|
|
|
;; $log_prefix_name: build "<prefix><suffix>" in scratch buf 0x300 and log it.
|
|
;; prefixPtr / prefixLen — the prefix string (e.g. "registered: ", 12 bytes)
|
|
;; suffixPtr / suffixLen — the name / pubkey to append
|
|
(func $log_prefix_name
|
|
(param $prefixPtr i32) (param $prefixLen i32)
|
|
(param $suffixPtr i32) (param $suffixLen i32)
|
|
;; copy prefix → scratch[0]
|
|
(call $memcpy (i32.const 0x300) (local.get $prefixPtr) (local.get $prefixLen))
|
|
;; copy suffix → scratch[prefixLen]
|
|
(call $memcpy
|
|
(i32.add (i32.const 0x300) (local.get $prefixLen))
|
|
(local.get $suffixPtr)
|
|
(local.get $suffixLen))
|
|
;; log scratch[0 .. prefixLen+suffixLen)
|
|
(call $log
|
|
(i32.const 0x300)
|
|
(i32.add (local.get $prefixLen) (local.get $suffixLen)))
|
|
)
|
|
|
|
;; $bytes_equal: compare mem[aPtr..aPtr+len) with mem[bPtr..bPtr+len)
|
|
;; Params: aPtr(0) bPtr(1) len(2)
|
|
;; Result: i32 1 = equal, 0 = not equal
|
|
(func $bytes_equal (param i32 i32 i32) (result i32)
|
|
(local $i i32)
|
|
(local $same i32)
|
|
(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 2)))
|
|
(if (i32.ne
|
|
(i32.load8_u (i32.add (local.get 0) (local.get $i)))
|
|
(i32.load8_u (i32.add (local.get 1) (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)
|
|
)
|
|
)
|
|
(local.get $same)
|
|
)
|
|
|
|
;; ── register(name) ────────────────────────────────────────────────────────
|
|
;; Claims `name` for the caller.
|
|
;; Logs "registered: <name>" on success, "name taken: <name>" on conflict.
|
|
(func (export "register")
|
|
(local $nameLen i32)
|
|
(local $callerLen i32)
|
|
(local $existingLen i32)
|
|
|
|
;; Read name into 0x000, max 64 bytes
|
|
(local.set $nameLen
|
|
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
|
(if (i32.eqz (local.get $nameLen)) (then return))
|
|
|
|
;; Check if name is already taken
|
|
(local.set $existingLen
|
|
(call $get_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x140) (i32.const 128)))
|
|
(if (i32.gt_u (local.get $existingLen) (i32.const 0))
|
|
(then
|
|
(call $log_prefix_name
|
|
(i32.const 0x20C) (i32.const 12) ;; "name taken: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
|
|
;; Store: state[name] = caller_pubkey
|
|
(local.set $callerLen
|
|
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
|
(call $set_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x0C0) (local.get $callerLen))
|
|
|
|
(call $log_prefix_name
|
|
(i32.const 0x200) (i32.const 12) ;; "registered: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
)
|
|
|
|
;; ── resolve(name) ─────────────────────────────────────────────────────────
|
|
;; Logs "owner: <pubkey>" for the registered name, or "not found: <name>".
|
|
(func (export "resolve")
|
|
(local $nameLen i32)
|
|
(local $ownerLen i32)
|
|
|
|
(local.set $nameLen
|
|
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
|
(if (i32.eqz (local.get $nameLen)) (then return))
|
|
|
|
(local.set $ownerLen
|
|
(call $get_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x140) (i32.const 128)))
|
|
(if (i32.eqz (local.get $ownerLen))
|
|
(then
|
|
(call $log_prefix_name
|
|
(i32.const 0x218) (i32.const 11) ;; "not found: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
;; Log "owner: <pubkey bytes>"
|
|
;; The pubkey stored in state is the raw caller pubkey bytes that the
|
|
;; host wrote via get_caller — these are the hex-encoded public key
|
|
;; string bytes, so the log will show the readable hex address.
|
|
(call $log_prefix_name
|
|
(i32.const 0x224) (i32.const 7) ;; "owner: "
|
|
(i32.const 0x140) (local.get $ownerLen))
|
|
)
|
|
|
|
;; ── transfer(name, new_owner) ─────────────────────────────────────────────
|
|
;; Transfers ownership of `name` to `new_owner`.
|
|
;; Only the current owner may call this (or anyone if name is unregistered).
|
|
;; Logs "transferred: <name>" on success, "unauthorized: <name>" otherwise.
|
|
(func (export "transfer")
|
|
(local $nameLen i32)
|
|
(local $newOwnerLen i32)
|
|
(local $callerLen i32)
|
|
(local $existingLen i32)
|
|
|
|
(local.set $nameLen
|
|
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
|
(if (i32.eqz (local.get $nameLen)) (then return))
|
|
|
|
(local.set $newOwnerLen
|
|
(call $get_arg_str (i32.const 1) (i32.const 0x040) (i32.const 128)))
|
|
(if (i32.eqz (local.get $newOwnerLen)) (then return))
|
|
|
|
;; Read existing owner into 0x140
|
|
(local.set $existingLen
|
|
(call $get_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x140) (i32.const 128)))
|
|
|
|
;; If not registered, anyone can claim → register directly for new_owner
|
|
(if (i32.eqz (local.get $existingLen))
|
|
(then
|
|
(call $set_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x040) (local.get $newOwnerLen))
|
|
(call $log_prefix_name
|
|
(i32.const 0x22C) (i32.const 13) ;; "transferred: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
|
|
;; Verify caller == existing owner
|
|
(local.set $callerLen
|
|
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
|
(if (i32.eqz
|
|
(call $bytes_equal
|
|
(i32.const 0x0C0) (i32.const 0x140)
|
|
(if (result i32) (i32.ne (local.get $callerLen) (local.get $existingLen))
|
|
(then (i32.const 0)) ;; length mismatch → not equal
|
|
(else (local.get $callerLen)))))
|
|
(then
|
|
(call $log_prefix_name
|
|
(i32.const 0x23A) (i32.const 14) ;; "unauthorized: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
|
|
;; Authorized — update owner
|
|
(call $set_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x040) (local.get $newOwnerLen))
|
|
(call $log_prefix_name
|
|
(i32.const 0x22C) (i32.const 13) ;; "transferred: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
)
|
|
|
|
;; ── release(name) ─────────────────────────────────────────────────────────
|
|
;; Removes a name registration. Only the current owner may call this.
|
|
;; Logs "released: <name>" on success.
|
|
(func (export "release")
|
|
(local $nameLen i32)
|
|
(local $callerLen i32)
|
|
(local $existingLen i32)
|
|
|
|
(local.set $nameLen
|
|
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
|
(if (i32.eqz (local.get $nameLen)) (then return))
|
|
|
|
(local.set $existingLen
|
|
(call $get_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x140) (i32.const 128)))
|
|
(if (i32.eqz (local.get $existingLen))
|
|
(then
|
|
(call $log_prefix_name
|
|
(i32.const 0x218) (i32.const 11) ;; "not found: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
|
|
;; Verify caller == owner
|
|
(local.set $callerLen
|
|
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
|
(if (i32.eqz
|
|
(call $bytes_equal
|
|
(i32.const 0x0C0) (i32.const 0x140)
|
|
(if (result i32) (i32.ne (local.get $callerLen) (local.get $existingLen))
|
|
(then (i32.const 0))
|
|
(else (local.get $callerLen)))))
|
|
(then
|
|
(call $log_prefix_name
|
|
(i32.const 0x23A) (i32.const 14) ;; "unauthorized: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
return
|
|
)
|
|
)
|
|
|
|
;; Store empty bytes → effectively deletes the record
|
|
(call $set_state
|
|
(i32.const 0x000) (local.get $nameLen)
|
|
(i32.const 0x000) (i32.const 0))
|
|
(call $log_prefix_name
|
|
(i32.const 0x249) (i32.const 10) ;; "released: "
|
|
(i32.const 0x000) (local.get $nameLen))
|
|
)
|
|
)
|