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
246 lines
7.6 KiB
Go
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
|
|
}
|