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

156
client-app/lib/crypto.ts Normal file
View File

@@ -0,0 +1,156 @@
/**
* Cryptographic operations for DChain messenger.
*
* Ed25519 — transaction signing (via TweetNaCl sign)
* X25519 — Diffie-Hellman key exchange for NaCl box
* NaCl box — authenticated encryption for relay messages
*/
import nacl from 'tweetnacl';
import { decodeUTF8, encodeUTF8 } from 'tweetnacl-util';
import { getRandomBytes } from 'expo-crypto';
import type { KeyFile } from './types';
// ─── PRNG ─────────────────────────────────────────────────────────────────────
// TweetNaCl looks for window.crypto which doesn't exist in React Native/Hermes.
// Wire nacl to expo-crypto which uses the platform's secure RNG natively.
nacl.setPRNG((output: Uint8Array, length: number) => {
const bytes = getRandomBytes(length);
for (let i = 0; i < length; i++) output[i] = bytes[i];
});
// ─── Helpers ──────────────────────────────────────────────────────────────────
export function hexToBytes(hex: string): Uint8Array {
if (hex.length % 2 !== 0) throw new Error('odd hex length');
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
export function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
// ─── Key generation ───────────────────────────────────────────────────────────
/**
* Generate a new identity: Ed25519 signing keys + X25519 encryption keys.
* Returns a KeyFile compatible with the Go node format.
*/
export function generateKeyFile(): KeyFile {
// Ed25519 for signing / blockchain identity
const signKP = nacl.sign.keyPair();
// X25519 for NaCl box encryption
// nacl.box.keyPair() returns Curve25519 keys
const boxKP = nacl.box.keyPair();
return {
pub_key: bytesToHex(signKP.publicKey),
priv_key: bytesToHex(signKP.secretKey),
x25519_pub: bytesToHex(boxKP.publicKey),
x25519_priv: bytesToHex(boxKP.secretKey),
};
}
// ─── NaCl box encryption ──────────────────────────────────────────────────────
/**
* Encrypt a plaintext message using NaCl box.
* Sender uses their X25519 secret key + recipient's X25519 public key.
* Returns { nonce, ciphertext } as hex strings.
*/
export function encryptMessage(
plaintext: string,
senderSecretHex: string,
recipientPubHex: string,
): { nonce: string; ciphertext: string } {
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const message = decodeUTF8(plaintext);
const secretKey = hexToBytes(senderSecretHex);
const publicKey = hexToBytes(recipientPubHex);
const box = nacl.box(message, nonce, publicKey, secretKey);
return {
nonce: bytesToHex(nonce),
ciphertext: bytesToHex(box),
};
}
/**
* Decrypt a NaCl box.
* Recipient uses their X25519 secret key + sender's X25519 public key.
*/
export function decryptMessage(
ciphertextHex: string,
nonceHex: string,
senderPubHex: string,
recipientSecHex: string,
): string | null {
try {
const ciphertext = hexToBytes(ciphertextHex);
const nonce = hexToBytes(nonceHex);
const senderPub = hexToBytes(senderPubHex);
const secretKey = hexToBytes(recipientSecHex);
const plain = nacl.box.open(ciphertext, nonce, senderPub, secretKey);
if (!plain) return null;
return encodeUTF8(plain);
} catch {
return null;
}
}
// ─── Ed25519 signing ──────────────────────────────────────────────────────────
/**
* Sign arbitrary data with the Ed25519 private key.
* Returns signature as hex.
*/
export function sign(data: Uint8Array, privKeyHex: string): string {
const secretKey = hexToBytes(privKeyHex);
const sig = nacl.sign.detached(data, secretKey);
return bytesToHex(sig);
}
/**
* Sign arbitrary data with the Ed25519 private key.
* Returns signature as base64 — this is the format the Go blockchain node
* expects ([]byte fields are base64 in JSON).
*/
export function signBase64(data: Uint8Array, privKeyHex: string): string {
const secretKey = hexToBytes(privKeyHex);
const sig = nacl.sign.detached(data, secretKey);
return bytesToBase64(sig);
}
/** Encode bytes as base64. Works on Hermes (btoa is available since RN 0.71). */
export function bytesToBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Verify an Ed25519 signature.
*/
export function verify(data: Uint8Array, sigHex: string, pubKeyHex: string): boolean {
try {
return nacl.sign.detached.verify(data, hexToBytes(sigHex), hexToBytes(pubKeyHex));
} catch {
return false;
}
}
// ─── Address helpers ──────────────────────────────────────────────────────────
/** Truncate a long hex address for display: 8...8 */
export function shortAddr(hex: string, chars = 8): string {
if (hex.length <= chars * 2 + 3) return hex;
return `${hex.slice(0, chars)}${hex.slice(-chars)}`;
}