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:
221
wallet/wallet.go
Normal file
221
wallet/wallet.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Package wallet manages Ed25519 keypairs with human-readable addresses,
|
||||
// encrypted persistence, and two usage profiles:
|
||||
//
|
||||
// - NodeWallet — bound to a running validator node; earns block/relay rewards.
|
||||
// - UserWallet — holds tokens for regular users; spends on transactions.
|
||||
//
|
||||
// Address format: "DC" + lowercase hex(sha256(pubkey)[0:12])
|
||||
// Example: DC3a7f2b1c9d0e5f6a7b8c9dab1f
|
||||
// (2 + 24 = 26 characters, collision-resistant for this use case)
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
|
||||
"go-blockchain/identity"
|
||||
)
|
||||
|
||||
// WalletType indicates how the wallet is used.
|
||||
type WalletType string
|
||||
|
||||
const (
|
||||
NodeWallet WalletType = "NODE" // validator node; earns rewards
|
||||
UserWallet WalletType = "USER" // end-user; spends tokens
|
||||
)
|
||||
|
||||
// Wallet holds an Ed25519 identity plus metadata.
|
||||
type Wallet struct {
|
||||
Type WalletType
|
||||
ID *identity.Identity
|
||||
Label string // human-readable nickname
|
||||
Address string // "DC..." address derived from pub key
|
||||
}
|
||||
|
||||
// New creates a fresh wallet of the given type and label.
|
||||
func New(wtype WalletType, label string) (*Wallet, error) {
|
||||
id, err := identity.Generate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Wallet{
|
||||
Type: wtype,
|
||||
ID: id,
|
||||
Label: label,
|
||||
Address: PubKeyToAddress(id.PubKeyHex()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FromIdentity wraps an existing identity in a Wallet.
|
||||
func FromIdentity(id *identity.Identity, wtype WalletType, label string) *Wallet {
|
||||
return &Wallet{
|
||||
Type: wtype,
|
||||
ID: id,
|
||||
Label: label,
|
||||
Address: PubKeyToAddress(id.PubKeyHex()),
|
||||
}
|
||||
}
|
||||
|
||||
// PubKeyToAddress derives the "DC…" address from a hex-encoded Ed25519 public key.
|
||||
func PubKeyToAddress(pubKeyHex string) string {
|
||||
raw, err := hex.DecodeString(pubKeyHex)
|
||||
if err != nil {
|
||||
return "DCinvalid"
|
||||
}
|
||||
h := sha256.Sum256(raw)
|
||||
return "DC" + hex.EncodeToString(h[:12])
|
||||
}
|
||||
|
||||
// AddressToPubKeyPrefix returns the first 12 bytes of the address payload (for display).
|
||||
func AddressToPubKeyPrefix(addr string) string {
|
||||
if len(addr) < 2 {
|
||||
return addr
|
||||
}
|
||||
return addr[2:] // strip "DC" prefix
|
||||
}
|
||||
|
||||
// --- persistence ---
|
||||
|
||||
// walletFile is the JSON structure saved to disk.
|
||||
// The private key is stored AES-256-GCM encrypted with Argon2id key derivation.
|
||||
type walletFile struct {
|
||||
Version int `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Address string `json:"address"`
|
||||
PubKey string `json:"pub_key"`
|
||||
|
||||
// Encryption envelope
|
||||
KDFSalt string `json:"kdf_salt"` // hex-encoded 16-byte Argon2id salt
|
||||
Nonce string `json:"nonce"` // hex-encoded 12-byte AES-GCM nonce
|
||||
EncPriv string `json:"enc_priv"` // hex-encoded AES-256-GCM ciphertext of priv key
|
||||
}
|
||||
|
||||
// Save encrypts the wallet and writes it to path.
|
||||
// passphrase may be "" (unencrypted, not recommended for production).
|
||||
func (w *Wallet) Save(path, passphrase string) error {
|
||||
privKeyBytes := []byte(hex.EncodeToString(w.ID.PrivKey))
|
||||
|
||||
salt := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return fmt.Errorf("generate salt: %w", err)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return fmt.Errorf("generate nonce: %w", err)
|
||||
}
|
||||
|
||||
key := deriveKey(passphrase, salt)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct := gcm.Seal(nil, nonce, privKeyBytes, nil)
|
||||
|
||||
wf := walletFile{
|
||||
Version: 1,
|
||||
Type: string(w.Type),
|
||||
Label: w.Label,
|
||||
Address: w.Address,
|
||||
PubKey: w.ID.PubKeyHex(),
|
||||
KDFSalt: hex.EncodeToString(salt),
|
||||
Nonce: hex.EncodeToString(nonce),
|
||||
EncPriv: hex.EncodeToString(ct),
|
||||
}
|
||||
data, err := json.MarshalIndent(wf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
// Load decrypts a wallet file.
|
||||
func Load(path, passphrase string) (*Wallet, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read wallet: %w", err)
|
||||
}
|
||||
var wf walletFile
|
||||
if err := json.Unmarshal(data, &wf); err != nil {
|
||||
return nil, fmt.Errorf("parse wallet: %w", err)
|
||||
}
|
||||
|
||||
salt, err := hex.DecodeString(wf.KDFSalt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
nonce, err := hex.DecodeString(wf.Nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode nonce: %w", err)
|
||||
}
|
||||
ct, err := hex.DecodeString(wf.EncPriv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode ciphertext: %w", err)
|
||||
}
|
||||
|
||||
key := deriveKey(passphrase, salt)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privKeyHex, err := gcm.Open(nil, nonce, ct, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("decryption failed: wrong passphrase?")
|
||||
}
|
||||
|
||||
id, err := identity.FromHex(wf.PubKey, string(privKeyHex))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load identity: %w", err)
|
||||
}
|
||||
return &Wallet{
|
||||
Type: WalletType(wf.Type),
|
||||
ID: id,
|
||||
Label: wf.Label,
|
||||
Address: wf.Address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deriveKey derives a 32-byte AES key from passphrase + salt using Argon2id.
|
||||
func deriveKey(passphrase string, salt []byte) []byte {
|
||||
// Argon2id parameters: time=1, memory=64MB, threads=4
|
||||
return argon2.IDKey([]byte(passphrase), salt, 1, 64*1024, 4, 32)
|
||||
}
|
||||
|
||||
// --- display helpers ---
|
||||
|
||||
// Info returns a map suitable for JSON marshalling.
|
||||
func (w *Wallet) Info() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": string(w.Type),
|
||||
"label": w.Label,
|
||||
"address": w.Address,
|
||||
"pub_key": w.ID.PubKeyHex(),
|
||||
}
|
||||
}
|
||||
|
||||
// Short returns "label (DCxxxx…xxxx)".
|
||||
func (w *Wallet) Short() string {
|
||||
addr := w.Address
|
||||
if len(addr) > 10 {
|
||||
addr = addr[:6] + "…" + addr[len(addr)-4:]
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", w.Label, addr)
|
||||
}
|
||||
Reference in New Issue
Block a user