Files
dchain/identity/identity.go
vsecoder 7e7393e4f8 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
2026-04-17 14:16:44 +03:00

246 lines
7.6 KiB
Go

package identity
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"go-blockchain/blockchain"
)
// Identity holds an Ed25519 keypair and a Curve25519 (X25519) keypair.
// Ed25519 is used for signing transactions and consensus messages.
// X25519 is used for NaCl box (E2E) message encryption.
type Identity struct {
PubKey ed25519.PublicKey
PrivKey ed25519.PrivateKey
// X25519 keypair for NaCl box encryption.
// Generated together with Ed25519; stored alongside in key files.
X25519Pub [32]byte
X25519Priv [32]byte
}
// Generate creates a fresh Ed25519 + X25519 keypair.
func Generate() (*Identity, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate ed25519: %w", err)
}
xpub, xpriv, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate x25519: %w", err)
}
return &Identity{
PubKey: pub,
PrivKey: priv,
X25519Pub: *xpub,
X25519Priv: *xpriv,
}, nil
}
// PubKeyHex returns the hex-encoded Ed25519 public key.
func (id *Identity) PubKeyHex() string {
return hex.EncodeToString(id.PubKey)
}
// PrivKeyHex returns the hex-encoded Ed25519 private key.
func (id *Identity) PrivKeyHex() string {
return hex.EncodeToString(id.PrivKey)
}
// X25519PubHex returns the hex-encoded Curve25519 public key.
func (id *Identity) X25519PubHex() string {
return hex.EncodeToString(id.X25519Pub[:])
}
// X25519PrivHex returns the hex-encoded Curve25519 private key.
func (id *Identity) X25519PrivHex() string {
return hex.EncodeToString(id.X25519Priv[:])
}
// Sign returns an Ed25519 signature over msg.
func (id *Identity) Sign(msg []byte) []byte {
return ed25519.Sign(id.PrivKey, msg)
}
// Verify returns true if sig is a valid Ed25519 signature over msg by pubKeyHex.
func Verify(pubKeyHex string, msg, sig []byte) (bool, error) {
pubBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return false, fmt.Errorf("invalid pub key hex: %w", err)
}
return ed25519.Verify(ed25519.PublicKey(pubBytes), msg, sig), nil
}
// MineRegistration performs a lightweight proof-of-work so that
// identity registration has a CPU cost (Sybil barrier).
func MineRegistration(pubKeyHex string, difficulty int) (nonce uint64, target string, err error) {
prefix := strings.Repeat("0", difficulty/4)
pubBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return 0, "", err
}
buf := make([]byte, len(pubBytes)+8)
copy(buf, pubBytes)
for nonce = 0; ; nonce++ {
binary.BigEndian.PutUint64(buf[len(pubBytes):], nonce)
h := sha256.Sum256(buf)
hexHash := hex.EncodeToString(h[:])
if strings.HasPrefix(hexHash, prefix) {
return nonce, hexHash, nil
}
}
}
// RegisterTx builds and signs a REGISTER_KEY transaction.
// It includes the X25519 public key so recipients can look it up on-chain.
func RegisterTx(id *Identity, nickname string, powDifficulty int) (*blockchain.Transaction, error) {
nonce, target, err := MineRegistration(id.PubKeyHex(), powDifficulty)
if err != nil {
return nil, err
}
payload := blockchain.RegisterKeyPayload{
PubKey: id.PubKeyHex(),
Nickname: nickname,
PowNonce: nonce,
PowTarget: target,
X25519PubKey: id.X25519PubHex(),
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, err
}
tx := &blockchain.Transaction{
ID: txID(id.PubKeyHex(), blockchain.EventRegisterKey),
Type: blockchain.EventRegisterKey,
From: id.PubKeyHex(),
Payload: payloadBytes,
Fee: blockchain.RegistrationFee,
Timestamp: time.Now().UTC(),
}
tx.Signature = id.Sign(txSignBytes(tx))
return tx, nil
}
// SignMessage returns a detached Ed25519 signature over an arbitrary message.
func (id *Identity) SignMessage(msg []byte) []byte {
return id.Sign(msg)
}
// VerifyMessage verifies a detached signature (see SignMessage).
func VerifyMessage(pubKeyHex string, msg, sig []byte) (bool, error) {
return Verify(pubKeyHex, msg, sig)
}
// FromHex reconstructs an Identity from hex-encoded Ed25519 keys.
// X25519 fields are left zeroed; use FromHexFull for complete identity.
func FromHex(pubHex, privHex string) (*Identity, error) {
return FromHexFull(pubHex, privHex, "", "")
}
// deriveX25519 deterministically derives a Curve25519 keypair from an Ed25519
// private key using the standard Ed25519→X25519 conversion (SHA-512 of seed +
// X25519 clamping). This matches libsodium's crypto_sign_ed25519_sk_to_curve25519.
func deriveX25519(privKey ed25519.PrivateKey) (pub [32]byte, priv [32]byte) {
// Ed25519 private key = seed (32 bytes) || public key (32 bytes).
seed := privKey[:32]
h := sha512.Sum512(seed)
// Apply X25519 scalar clamping.
h[0] &= 248
h[31] &= 127
h[31] |= 64
copy(priv[:], h[:32])
pubSlice, _ := curve25519.X25519(priv[:], curve25519.Basepoint)
copy(pub[:], pubSlice)
return pub, priv
}
// FromHexFull reconstructs a complete Identity including X25519 keys.
// When x25519PubHex/x25519PrivHex are empty, X25519 is derived deterministically
// from the Ed25519 private key using the standard Ed25519→X25519 conversion.
func FromHexFull(pubHex, privHex, x25519PubHex, x25519PrivHex string) (*Identity, error) {
pubBytes, err := hex.DecodeString(pubHex)
if err != nil {
return nil, fmt.Errorf("decode pub key: %w", err)
}
privBytes, err := hex.DecodeString(privHex)
if err != nil {
return nil, fmt.Errorf("decode priv key: %w", err)
}
id := &Identity{
PubKey: ed25519.PublicKey(pubBytes),
PrivKey: ed25519.PrivateKey(privBytes),
}
if x25519PubHex != "" && x25519PrivHex != "" {
b, err := hex.DecodeString(x25519PubHex)
if err != nil || len(b) != 32 {
return nil, fmt.Errorf("decode x25519 pub key: %w", err)
}
copy(id.X25519Pub[:], b)
b, err = hex.DecodeString(x25519PrivHex)
if err != nil || len(b) != 32 {
return nil, fmt.Errorf("decode x25519 priv key: %w", err)
}
copy(id.X25519Priv[:], b)
} else {
// Derive X25519 deterministically from Ed25519 private key.
id.X25519Pub, id.X25519Priv = deriveX25519(id.PrivKey)
}
return id, nil
}
// txID generates a deterministic transaction ID.
func txID(fromPubHex string, eventType blockchain.EventType) string {
h := sha256.Sum256([]byte(fromPubHex + string(eventType) + fmt.Sprint(time.Now().UnixNano())))
return hex.EncodeToString(h[:16])
}
// TxSignBytes returns the canonical bytes that must be signed (and verified)
// for a transaction. Use this whenever building a transaction outside of the
// identity package — signing json.Marshal(tx) instead is a common mistake
// that produces signatures VerifyTx will always reject.
func TxSignBytes(tx *blockchain.Transaction) []byte { return txSignBytes(tx) }
// txSignBytes returns the canonical bytes that are signed for a transaction.
func txSignBytes(tx *blockchain.Transaction) []byte {
data, _ := json.Marshal(struct {
ID string `json:"id"`
Type blockchain.EventType `json:"type"`
From string `json:"from"`
To string `json:"to"`
Amount uint64 `json:"amount"`
Fee uint64 `json:"fee"`
Payload []byte `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}{
tx.ID, tx.Type, tx.From, tx.To, tx.Amount, tx.Fee, tx.Payload, tx.Timestamp,
})
return data
}
// VerifyTx verifies a transaction's Ed25519 signature.
func VerifyTx(tx *blockchain.Transaction) error {
ok, err := Verify(tx.From, txSignBytes(tx), tx.Signature)
if err != nil {
return err
}
if !ok {
return errors.New("transaction signature invalid")
}
return nil
}