commit inicial do projeto

This commit is contained in:
Júnior
2025-05-23 10:44:32 -03:00
commit 8f04473c0b
106 changed files with 5673 additions and 0 deletions

View File

@ -0,0 +1,82 @@
package api
import (
"dejo_node/internal/state"
"dejo_node/internal/storage"
"encoding/json"
"net/http"
"strings"
)
var GlobalState *state.State
var GlobalBlockStore *storage.BlockStore
func HandleGetBalance(w http.ResponseWriter, r *http.Request) {
addr := strings.TrimPrefix(r.URL.Path, "/accounts/")
if strings.Contains(addr, "/") {
addr = strings.Split(addr, "/")[0]
}
if addr == "" {
http.Error(w, "endereço inválido", http.StatusBadRequest)
return
}
balance := GlobalState.GetBalance(addr)
resp := map[string]interface{}{
"address": addr,
"balance": balance,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func HandleGetTransactionsByAddress(w http.ResponseWriter, r *http.Request) {
addr := strings.TrimPrefix(r.URL.Path, "/accounts/")
addr = strings.TrimSuffix(addr, "/txs")
if addr == "" {
http.Error(w, "endereço inválido", http.StatusBadRequest)
return
}
txs := []map[string]interface{}{}
blocks, err := GlobalBlockStore.LoadAll()
if err != nil {
http.Error(w, "erro ao carregar blocos", http.StatusInternalServerError)
return
}
for _, blk := range blocks {
for _, tx := range blk.Txns {
if tx.From == addr || tx.To == addr {
txs = append(txs, map[string]interface{}{
"from": tx.From,
"to": tx.To,
"value": tx.Value,
"hash": tx.Hash(),
"block": blk.Index,
})
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(txs)
}
func (h *Handler) GetTransactionByHash(w http.ResponseWriter, r *http.Request) {
hash := strings.TrimPrefix(r.URL.Path, "/transaction/")
blocks, err := h.Store.LoadAll()
if err != nil {
http.Error(w, "erro ao carregar blocos", http.StatusInternalServerError)
return
}
for _, blk := range blocks {
for _, tx := range blk.Txns {
if tx.Hash() == hash {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tx)
return
}
}
}
http.Error(w, "transação não encontrada", http.StatusNotFound)
}

View File

@ -0,0 +1,31 @@
package api
import (
"net/http"
"strconv"
"github.com/gorilla/mux"
)
func (h *Handler) ListBlocks(w http.ResponseWriter, r *http.Request) {
blocks, err := h.Store.ListBlocks()
if err != nil {
WriteError(w, http.StatusInternalServerError, "Erro ao listar blocos")
return
}
WriteJSON(w, blocks)
}
func (h *Handler) GetBlockByHeight(w http.ResponseWriter, r *http.Request) {
heightStr := mux.Vars(r)["height"]
height, err := strconv.Atoi(heightStr)
if err != nil {
WriteError(w, http.StatusBadRequest, "Altura inválida")
return
}
block, err := h.Store.LoadBlock(height)
if err != nil {
WriteError(w, http.StatusNotFound, "Bloco não encontrado")
return
}
WriteJSON(w, block)
}

15
internal/api/dao_api.go Normal file
View File

@ -0,0 +1,15 @@
package api
import (
"dejo_node/internal/dao"
"sync/atomic"
)
// DaoStore é acessado globalmente pelos handlers DAO.
var DaoStore *dao.ProposalStore
// MinStakePtr é o ponteiro global para o valor atual de minStake.
var MinStakePtr *atomic.Uint64
// SetMinStakeFunc permite alterar dinamicamente via DAO.
var SetMinStakeFunc func(uint64)

View File

@ -0,0 +1,84 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"dejo_node/internal/dao"
)
type CreateProposalRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Type string `json:"type"`
Creator string `json:"creator"`
Duration int64 `json:"duration"`
}
type VoteRequest struct {
Address string `json:"address"`
Approve bool `json:"approve"`
}
func (h *Handler) CreateProposal(w http.ResponseWriter, r *http.Request) {
var req CreateProposalRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "JSON inválido")
return
}
p := DaoStore.Create(req.Title, req.Content, req.Creator, dao.ProposalType(req.Type), req.Duration)
_ = DaoStore.SaveToDisk("data/proposals.db")
WriteJSON(w, p)
}
func (h *Handler) VoteProposal(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
WriteError(w, http.StatusBadRequest, "ID inválido")
return
}
var req VoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "JSON inválido")
return
}
p, err := DaoStore.Get(id)
if err != nil {
WriteError(w, http.StatusNotFound, "Proposta não encontrada")
return
}
_ = p.Vote(req.Address, req.Approve)
p.CheckAndClose(h.snapshotStakes())
_ = DaoStore.SaveToDisk("data/proposals.db")
WriteJSON(w, p)
}
func (h *Handler) GetProposal(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
WriteError(w, http.StatusBadRequest, "ID inválido")
return
}
p, err := DaoStore.Get(id)
if err != nil {
WriteError(w, http.StatusNotFound, "Proposta não encontrada")
return
}
WriteJSON(w, p)
}
func (h *Handler) ListProposals(w http.ResponseWriter, r *http.Request) {
WriteJSON(w, DaoStore.List())
}
func (h *Handler) snapshotStakes() map[string]uint64 {
snapshot := make(map[string]uint64)
for addr, entry := range h.StakingStore.Snapshot() {
snapshot[addr] = entry.Amount
}
return snapshot
}

20
internal/api/handler.go Normal file
View File

@ -0,0 +1,20 @@
package api
import (
"dejo_node/internal/mempool"
"dejo_node/internal/staking"
"dejo_node/internal/storage"
)
type Handler struct {
Store *storage.BlockStore
StakingStore *staking.StakingStore
Mempool *mempool.Mempool
}
func NewHandler() *Handler {
return &Handler{
Store: storage.NewBlockStore("data/blocks.json"),
StakingStore: staking.NewStakingStore(),
}
}

10
internal/api/health.go Normal file
View File

@ -0,0 +1,10 @@
package api
import (
"net/http"
)
func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}

View File

@ -0,0 +1,11 @@
package api
import (
"net/http"
)
// HandlePing responde a heartbeat dos peers
func HandlePing(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("pong"))
}

15
internal/api/liveness.go Normal file
View File

@ -0,0 +1,15 @@
package api
import (
"net/http"
)
func (h *Handler) Startup(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("started"))
}
func (h *Handler) Ready(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ready"))
}

View File

@ -0,0 +1,91 @@
package api
import (
"context"
"net"
"net/http"
"strings"
"sync"
"time"
limiter "github.com/ulule/limiter/v3"
memory "github.com/ulule/limiter/v3/drivers/store/memory"
stdmiddleware "github.com/ulule/limiter/v3/drivers/middleware/stdlib"
)
// SybilProtector mantém IPs bloqueados temporariamente
var sybilBlocklist = struct {
mu sync.RWMutex
block map[string]time.Time
}{block: make(map[string]time.Time)}
// isBlocked verifica se um IP está na lista de bloqueio
func isBlocked(ip string) bool {
sybilBlocklist.mu.RLock()
defer sybilBlocklist.mu.RUnlock()
expire, exists := sybilBlocklist.block[ip]
return exists && time.Now().Before(expire)
}
// blockIP adiciona um IP à blocklist por X segundos
func blockIP(ip string, duration time.Duration) {
sybilBlocklist.mu.Lock()
defer sybilBlocklist.mu.Unlock()
sybilBlocklist.block[ip] = time.Now().Add(duration)
}
// extractIP extrai IP puro do RemoteAddr
func extractIP(addr string) string {
ip, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return ip
}
// NewRateLimiterMiddleware cria middleware de rate limiting e proteção anti-Sybil
func NewRateLimiterMiddleware() func(http.Handler) http.Handler {
rate := limiter.Rate{
Period: 1 * time.Second,
Limit: 5,
}
store := memory.NewStore()
limiterInstance := limiter.New(store, rate)
middleware := stdmiddleware.NewMiddleware(limiterInstance)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Ignorar WebSocket
if strings.HasPrefix(r.URL.Path, "/ws") {
next.ServeHTTP(w, r)
return
}
ip := extractIP(r.RemoteAddr)
if isBlocked(ip) {
w.WriteHeader(http.StatusTooManyRequests)
WriteError(w, http.StatusTooManyRequests, "IP bloqueado temporariamente por abuso")
return
}
ctx := context.WithValue(r.Context(), "real-ip", ip)
rec := &responseRecorder{ResponseWriter: w, status: 200}
middleware.Handler(next).ServeHTTP(rec, r.WithContext(ctx))
if rec.status == http.StatusTooManyRequests {
blockIP(ip, 30*time.Second)
}
})
}
}
// responseRecorder intercepta status code para detectar excesso de requisições
type responseRecorder struct {
http.ResponseWriter
status int
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.status = code
rr.ResponseWriter.WriteHeader(code)
}

42
internal/api/router.go Normal file
View File

@ -0,0 +1,42 @@
package api
import (
"github.com/gorilla/mux"
"net/http"
)
func NewRouter(h *Handler) http.Handler {
r := mux.NewRouter()
r.HandleFunc("/health", h.Health).Methods("GET")
r.HandleFunc("/startup", h.Startup).Methods("GET")
r.HandleFunc("/ready", h.Ready).Methods("GET")
// ⚡ Transações
r.HandleFunc("/transaction", h.SendTransaction).Methods("POST")
r.HandleFunc("/transaction/{hash}", h.GetTransactionByHash).Methods("GET")
// 🔐 Staking
r.HandleFunc("/stake", h.StakeHandler).Methods("POST")
r.HandleFunc("/unstake", h.UnstakeHandler).Methods("POST")
r.HandleFunc("/stake/{address}", h.GetStakeInfo).Methods("GET")
// 🗳️ DAO
r.HandleFunc("/dao/proposals", h.CreateProposal).Methods("POST")
r.HandleFunc("/dao/proposals/{id}/vote", h.VoteProposal).Methods("POST")
r.HandleFunc("/dao/proposals", h.ListProposals).Methods("GET")
r.HandleFunc("/dao/proposals/{id}", h.GetProposal).Methods("GET")
// 💰 Accounts
r.HandleFunc("/accounts/{address}", HandleGetBalance).Methods("GET")
r.HandleFunc("/accounts/{address}/txs", HandleGetTransactionsByAddress).Methods("GET")
// 📦 Blocos
r.HandleFunc("/blocks", h.ListBlocks).Methods("GET")
r.HandleFunc("/blocks/{height}", h.GetBlockByHeight).Methods("GET")
// ❤️ Heartbeat
r.HandleFunc("/ping", HandlePing).Methods("GET")
return r
}

21
internal/api/server.go Normal file
View File

@ -0,0 +1,21 @@
package api
import (
"github.com/gorilla/mux"
"net/http"
"dejo_node/internal/ws"
)
func NewServer(handler *Handler) http.Handler {
r := mux.NewRouter()
// ⚠️ Endpoints removidos temporariamente pois ainda não foram implementados
// r.HandleFunc("/block/latest", handler.GetLatestBlock).Methods("GET")
// r.HandleFunc("/block/{index}", handler.GetBlockByIndex).Methods("GET")
// r.HandleFunc("/tx/{hash}", handler.GetTransactionByHash).Methods("GET")
// r.HandleFunc("/tx", handler.PostTransaction).Methods("POST")
// r.HandleFunc("/oracle/{key}", handler.GetOracleValue).Methods("GET")
r.HandleFunc("/ws", ws.HandleWS).Methods("GET")
return r
}

View File

@ -0,0 +1,53 @@
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
type StakeRequest struct {
Address string `json:"address"`
Amount uint64 `json:"amount"`
Duration int64 `json:"duration"`
}
type UnstakeRequest struct {
Address string `json:"address"`
}
func (h *Handler) StakeHandler(w http.ResponseWriter, r *http.Request) {
var req StakeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "Invalid request")
return
}
if err := h.StakingStore.Stake(req.Address, req.Amount, uint64(req.Duration)); err != nil {
WriteError(w, http.StatusInternalServerError, "Failed to stake")
return
}
WriteJSON(w, map[string]string{"message": "Stake registered successfully"})
}
func (h *Handler) UnstakeHandler(w http.ResponseWriter, r *http.Request) {
var req UnstakeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
WriteError(w, http.StatusBadRequest, "Invalid request")
return
}
if err := h.StakingStore.Unstake(req.Address); err != nil {
WriteError(w, http.StatusInternalServerError, "Failed to unstake")
return
}
WriteJSON(w, map[string]string{"message": "Unstake successful"})
}
func (h *Handler) GetStakeInfo(w http.ResponseWriter, r *http.Request) {
address := mux.Vars(r)["address"]
info, ok := h.StakingStore.GetStakeInfo(address)
if !ok {
WriteError(w, http.StatusNotFound, "Stake info not found")
return
}
WriteJSON(w, info)
}

View File

@ -0,0 +1,30 @@
package api
import (
"encoding/json"
"log"
"net/http"
"dejo_node/internal/transactions"
)
func (h *Handler) SendTransaction(w http.ResponseWriter, r *http.Request) {
log.Println("📥 Nova transação recebida")
var tx transactions.Transaction
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&tx); err != nil {
http.Error(w, "formato inválido", http.StatusBadRequest)
return
}
if tx.From == "" || tx.To == "" || tx.Value <= 0 {
http.Error(w, "transação inválida", http.StatusBadRequest)
return
}
h.Mempool.Add(&tx)
log.Printf("✅ Transação adicionada ao mempool: %+v\n", tx)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

22
internal/api/utils.go Normal file
View File

@ -0,0 +1,22 @@
package api
import (
"encoding/json"
"net/http"
)
func WriteError(w http.ResponseWriter, code int, msg string) {
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": msg,
})
}
func WriteJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"data": data,
})
}

View File

@ -0,0 +1,13 @@
package blockchain
import "dejo_node/internal/transactions"
// Block representa um bloco da blockchain.
type Block struct {
Hash string
PreviousHash string
Timestamp int64
Transactions []transactions.Transaction
Nonce int
Finalized bool
}

27
internal/config/config.go Normal file
View File

@ -0,0 +1,27 @@
package config
import (
"crypto/ecdsa"
"sync"
)
var (
GlobalPrivateKey *ecdsa.PrivateKey
GlobalPublicKey *ecdsa.PublicKey
once sync.Once
)
// SetGlobalKeys configura a chave privada e pública globais, se ainda não estiverem definidas.
func SetGlobalKeys(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey) {
once.Do(func() {
GlobalPrivateKey = priv
GlobalPublicKey = pub
})
}
// ResetGlobalKeys limpa as chaves globais (usado em testes)
func ResetGlobalKeys() {
GlobalPrivateKey = nil
GlobalPublicKey = nil
once = sync.Once{}
}

View File

@ -0,0 +1,15 @@
package consensus
import "dejo_node/internal/transactions"
// ConsensusEngine define a interface para algoritmos de consenso.
type ConsensusEngine interface {
// ValidateBlock verifica se o bloco atende aos critérios do consenso.
ValidateBlock(block *transactions.Block) error
// SelectProposer retorna o ID do validador responsável por propor o próximo bloco.
SelectProposer(height uint64) (string, error)
// FinalizeBlock aplica qualquer regra de finalização (ex: selar, assinar, etc).
FinalizeBlock(block *transactions.Block) error
}

View File

@ -0,0 +1,15 @@
package consensus
import "dejo_node/internal/transactions"
// Engine representa um mecanismo de consenso pluggable.
type Engine interface {
// CanPropose determina se o nó atual pode propor um novo bloco
CanPropose() bool
// Finalize valida e finaliza o bloco antes de ser adicionado na cadeia
Finalize(block *transactions.Block) error
// Name retorna o nome do mecanismo de consenso ativo
Name() string
}

View File

@ -0,0 +1,72 @@
package consensus
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/big"
"time"
"dejo_node/internal/transactions"
)
// Finalizer encapsula a lógica de selar e assinar blocos.
type Finalizer struct {
PrivateKey *ecdsa.PrivateKey
NodeID string // opcional: ID do produtor do bloco
}
// Finalize executa a finalização de um bloco:
// - Adiciona timestamp
// - Gera hash final
// - Retorna assinatura
func (f *Finalizer) Finalize(block *transactions.Block) (string, error) {
if block == nil {
return "", errors.New("bloco nulo")
}
block.Timestamp = time.Now().Unix()
// Geração do hash usando campos existentes do bloco
hashInput := []byte(
fmt.Sprintf("%d|%s|%d|%s",
block.Index,
block.PrevHash,
block.Timestamp,
f.NodeID,
),
)
hash := sha256.Sum256(hashInput)
block.Hash = hex.EncodeToString(hash[:])
r, s, err := ecdsa.Sign(rand.Reader, f.PrivateKey, hash[:])
if err != nil {
return "", err
}
sig := append(r.Bytes(), s.Bytes()...)
signature := hex.EncodeToString(sig)
return signature, nil
}
// VerifySignature verifica se a assinatura é válida com a chave pública fornecida.
func VerifySignature(block *transactions.Block, signature string, pubKey *ecdsa.PublicKey) bool {
hash, err := hex.DecodeString(block.Hash)
if err != nil {
return false
}
sig, err := hex.DecodeString(signature)
if err != nil || len(sig) < 64 {
return false
}
r := big.NewInt(0).SetBytes(sig[:len(sig)/2])
s := big.NewInt(0).SetBytes(sig[len(sig)/2:])
return ecdsa.Verify(pubKey, hash, r, s)
}

View File

@ -0,0 +1,78 @@
package consensus
import (
"log"
"os"
"strings"
"sync"
"time"
)
type LivenessMonitor struct {
Peers []string
Status map[string]bool
Mu sync.RWMutex
SelfID string
Transport *HTTPTransport
}
func NewLivenessMonitor(transport *HTTPTransport) *LivenessMonitor {
peersEnv := os.Getenv("DEJO_PEERS")
peers := []string{}
if peersEnv != "" {
peers = strings.Split(peersEnv, ",")
}
status := make(map[string]bool)
for _, peer := range peers {
status[peer] = true
}
return &LivenessMonitor{
Peers: peers,
Status: status,
Transport: transport,
SelfID: os.Getenv("NODE_ID"),
}
}
func (l *LivenessMonitor) Start() {
interval := 5 * time.Second
if val := os.Getenv("DEJO_LIVENESS_INTERVAL"); val != "" {
if d, err := time.ParseDuration(val); err == nil {
interval = d
}
}
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
l.checkPeers()
}
}()
}
func (l *LivenessMonitor) checkPeers() {
for _, peer := range l.Peers {
err := l.Transport.PingPeer(peer)
l.Mu.Lock()
if err != nil {
if l.Status[peer] {
log.Println("⚠️ Peer", peer, "está OFFLINE")
}
l.Status[peer] = false
} else {
if !l.Status[peer] {
log.Println("✅ Peer", peer, "voltou ONLINE")
}
l.Status[peer] = true
}
l.Mu.Unlock()
}
}
func (l *LivenessMonitor) IsAlive(peer string) bool {
if peer == l.SelfID {
return true
}
l.Mu.RLock()
defer l.Mu.RUnlock()
return l.Status[peer]
}

View File

@ -0,0 +1,54 @@
package consensus
import "time"
// MessageType representa o tipo de mensagem de consenso.
type MessageType string
const (
ProposalType MessageType = "PROPOSAL"
PrevoteType MessageType = "PREVOTE"
PrecommitType MessageType = "PRECOMMIT"
)
// ConsensusMessage representa uma mensagem genérica de consenso.
type ConsensusMessage interface {
Type() MessageType
Height() uint64
Round() uint64
ValidatorID() string
Timestamp() time.Time
}
// BaseMsg contém os campos comuns entre as mensagens.
type BaseMsg struct {
MsgType MessageType
HeightVal uint64
RoundVal uint64
Validator string
Time time.Time
}
func (b BaseMsg) Type() MessageType { return b.MsgType }
func (b BaseMsg) Height() uint64 { return b.HeightVal }
func (b BaseMsg) Round() uint64 { return b.RoundVal }
func (b BaseMsg) ValidatorID() string { return b.Validator }
func (b BaseMsg) Timestamp() time.Time { return b.Time }
// ProposalMsg carrega a proposta de bloco feita por um validador.
type ProposalMsg struct {
BaseMsg
BlockHash string // Hash do bloco proposto
}
// PrevoteMsg representa um voto inicial a favor de um bloco ou nil.
type PrevoteMsg struct {
BaseMsg
BlockHash string // Pode ser "" para nil
}
// PrecommitMsg representa o voto firme para travar o bloco.
type PrecommitMsg struct {
BaseMsg
BlockHash string // Pode ser "" para nil
}

View File

@ -0,0 +1,39 @@
package consensus
import (
"sync"
)
// MessageHandler é a função chamada quando uma mensagem é recebida.
type MessageHandler func(msg ConsensusMessage)
// Transport simula o envio e recebimento de mensagens de consenso entre nós.
type Transport struct {
handlersMu sync.RWMutex
handlers map[MessageType][]MessageHandler
}
// NewTransport cria um novo transporte de mensagens de consenso.
func NewTransport() *Transport {
return &Transport{
handlers: make(map[MessageType][]MessageHandler),
}
}
// Register adiciona um handler para um tipo de mensagem.
func (t *Transport) Register(msgType MessageType, handler MessageHandler) {
t.handlersMu.Lock()
defer t.handlersMu.Unlock()
t.handlers[msgType] = append(t.handlers[msgType], handler)
}
// Broadcast envia uma mensagem para todos os handlers registrados daquele tipo.
func (t *Transport) Broadcast(msg ConsensusMessage) {
t.handlersMu.RLock()
handlers := t.handlers[msg.Type()]
t.handlersMu.RUnlock()
for _, h := range handlers {
go h(msg) // Envia de forma assíncrona
}
}

53
internal/consensus/pos.go Normal file
View File

@ -0,0 +1,53 @@
package consensus
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"dejo_node/internal/transactions"
)
// PoSEngine é uma implementação simples e fake de um consenso Proof-of-Stake.
type PoSEngine struct {
validators []string
}
// NewPoSEngine cria um novo mecanismo de consenso PoS.
func NewPoSEngine(validators []string) *PoSEngine {
return &PoSEngine{
validators: validators,
}
}
// ValidateBlock verifica se o bloco tem dados válidos.
func (p *PoSEngine) ValidateBlock(block *transactions.Block) error {
if block == nil {
return errors.New("bloco nulo")
}
if block.Index == 0 && block.PrevHash != "" {
return errors.New("bloco gênese não deve ter PrevHash")
}
if len(block.Txns) == 0 {
return errors.New("bloco sem transações")
}
return nil
}
// SelectProposer seleciona pseudo-aleatoriamente um validador com base na altura.
func (p *PoSEngine) SelectProposer(height uint64) (string, error) {
if len(p.validators) == 0 {
return "", errors.New("nenhum validador registrado")
}
hash := sha256.Sum256([]byte(fmt.Sprintf("%d", height)))
index := int(hash[0]) % len(p.validators)
return p.validators[index], nil
}
// FinalizeBlock simula a finalização de bloco assinando seu hash.
func (p *PoSEngine) FinalizeBlock(block *transactions.Block) error {
hash := sha256.Sum256([]byte(fmt.Sprintf("%d:%s:%d", block.Index, block.PrevHash, len(block.Txns))))
block.Hash = hex.EncodeToString(hash[:])
return nil
}

View File

@ -0,0 +1,18 @@
package consensus
import (
"dejo_node/internal/mempool"
"dejo_node/internal/transactions"
"time"
)
func ProposeBlock(index uint64, prevHash string, pool *mempool.Mempool) *transactions.Block {
block := &transactions.Block{
Index: index,
PrevHash: prevHash,
Timestamp: time.Now().Unix(),
Txns: pool.All(),
}
block.Hash = block.CalculateHash()
return block
}

View File

@ -0,0 +1,55 @@
package consensus
import "log"
// CheckQuorum verifica se 2/3 dos validadores (por quantidade) assinaram.
func CheckQuorum(votes map[string]string, totalValidators int) (bool, string) {
voteCounts := make(map[string]int)
for _, hash := range votes {
voteCounts[hash]++
}
quorum := (2 * totalValidators) / 3
for hash, count := range voteCounts {
if count > quorum {
return true, hash
}
}
return false, ""
}
// CheckQuorumWeighted verifica se votos representam 2/3 do stake total considerando peers online.
func CheckQuorumWeighted(votes map[string]string, valSet *ValidatorSet, liveness *LivenessMonitor) (bool, string) {
totalStake := uint64(0)
for _, val := range valSet.Validators {
if liveness.IsAlive(val.Address) {
totalStake += val.Stake
}
}
required := (2 * totalStake) / 3
log.Printf("🔍 Total stake online: %d | Quórum necessário: %d\n", totalStake, required)
weighted := make(map[string]uint64)
for validator, hash := range votes {
val, ok := valSet.ValidatorByAddress(validator)
if !ok {
log.Printf("⚠️ Validador %s não encontrado no conjunto\n", validator)
continue
}
if !liveness.IsAlive(val.Address) {
log.Printf("⚠️ Validador %s está OFFLINE\n", val.Address)
continue
}
log.Printf("✅ Voto de %s para hash %s com %d tokens\n", val.Address, hash, val.Stake)
weighted[hash] += val.Stake
}
for hash, sum := range weighted {
log.Printf("📊 Hash %s recebeu %d tokens\n", hash, sum)
if sum > required {
log.Printf("🎯 Quórum atingido para hash %s\n", hash)
return true, hash
}
}
log.Println("❌ Quórum NÃO atingido")
return false, ""
}

View File

@ -0,0 +1,174 @@
package consensus
import (
"context"
"dejo_node/internal/mempool"
"dejo_node/internal/staking"
"dejo_node/internal/state"
"dejo_node/internal/storage"
"dejo_node/internal/transactions"
"log"
"time"
)
const (
PhaseProposal = "PROPOSAL"
PhasePrevote = "PREVOTE"
PhasePrecommit = "PRECOMMIT"
rewardAmount = 5
MaxRoundTimeout = 10 * time.Second
)
func StartConsensusLoop(
ctx context.Context,
nodeID string,
roundState *RoundState,
broadcast func(msg ConsensusMessage),
totalValidators int,
store *storage.BlockStore,
createBlockFn func() *transactions.Block,
stakingStore *staking.StakingStore,
minStake uint64,
liveness *LivenessMonitor,
) {
log.Println("🚀 Iniciando loop de consenso para altura", roundState.Height)
if store == nil {
log.Fatal("❌ ERRO: BlockStore está nil no StartConsensusLoop")
}
phase := PhaseProposal
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var proposedBlock *transactions.Block
validatorSet := NewValidatorSetFromStaking(stakingStore, minStake)
globalState := state.NewState()
_ = globalState.LoadFromDisk("data/state.gob")
pool := mempool.NewMempool()
if !validatorSet.IsValidator(nodeID) {
log.Println("⚠️ Este nó não é validador ativo — encerrando consenso nesta altura")
return
}
proposer := validatorSet.SelectProposer(roundState.Height)
if proposer == nil {
log.Println("❌ Nenhum propositor válido encontrado")
return
}
roundState.LastRoundStart = time.Now()
for {
select {
case <-ctx.Done():
log.Println("🛑 Loop de consenso encerrado")
return
case <-ticker.C:
roundState.Mu.Lock()
if time.Since(roundState.LastRoundStart) > MaxRoundTimeout {
log.Println("⏰ Timeout! Reiniciando round", roundState.Round+1)
roundState.ResetRound(roundState.Round + 1)
roundState.LastRoundStart = time.Now()
phase = PhaseProposal
roundState.Mu.Unlock()
continue
}
switch phase {
case PhaseProposal:
if proposer.Address != nodeID {
log.Println("⏳ Aguardando proposta do propositor", proposer.Address)
phase = PhasePrevote
break
}
log.Println("📤 Fase de PROPOSTA - propondo bloco")
latest, err := store.GetLatestBlock()
if err != nil {
log.Println("⚠️ Nenhum bloco encontrado, utilizando bloco base")
latest = &transactions.Block{Index: 0, Hash: "genesis"}
}
proposedBlock = ProposeBlock(uint64(latest.Index+1), latest.Hash, pool)
proposal := ProposalMsg{
BaseMsg: BaseMsg{
MsgType: ProposalType,
HeightVal: roundState.Height,
RoundVal: roundState.Round,
Validator: nodeID,
Time: time.Now(),
},
BlockHash: proposedBlock.Hash,
}
roundState.Proposal = proposedBlock.Hash
broadcast(proposal)
phase = PhasePrevote
case PhasePrevote:
log.Println("🗳️ Fase de PREVOTE")
vote := PrevoteMsg{
BaseMsg: BaseMsg{
MsgType: PrevoteType,
HeightVal: roundState.Height,
RoundVal: roundState.Round,
Validator: nodeID,
Time: time.Now(),
},
BlockHash: roundState.Proposal,
}
roundState.Prevotes[nodeID] = vote.BlockHash
broadcast(vote)
phase = PhasePrecommit
case PhasePrecommit:
log.Println("🔐 Fase de PRECOMMIT")
vote := PrecommitMsg{
BaseMsg: BaseMsg{
MsgType: PrecommitType,
HeightVal: roundState.Height,
RoundVal: roundState.Round,
Validator: nodeID,
Time: time.Now(),
},
BlockHash: roundState.Proposal,
}
roundState.Precommits[nodeID] = vote.BlockHash
broadcast(vote)
quorumReached, blockHash := CheckQuorumWeighted(roundState.Precommits, validatorSet, liveness)
if quorumReached {
log.Println("🎉 Quórum alcançado! Bloco finalizado:", blockHash)
if nodeID == proposer.Address && proposedBlock != nil && proposedBlock.Hash == blockHash {
totalVotedStake := uint64(0)
votedStakes := make(map[string]uint64)
for validatorID, votedHash := range roundState.Precommits {
if votedHash == blockHash && validatorSet.IsValidator(validatorID) {
val, _ := validatorSet.ValidatorByAddress(validatorID)
votedStakes[validatorID] = val.Stake
totalVotedStake += val.Stake
}
}
for validatorID, stake := range votedStakes {
reward := (stake * rewardAmount) / totalVotedStake
globalState.Mint(validatorID, reward)
log.Printf("💸 Recompensa dinâmica de %d tokens para %s\n", reward, validatorID)
}
_ = globalState.SaveToDisk("data/state.gob")
if err := store.SaveBlock(proposedBlock); err != nil {
log.Println("❌ Erro ao persistir bloco:", err)
} else {
log.Println("💾 Bloco persistido com sucesso! Height:", proposedBlock.Index)
pool.Clear()
}
}
ApplySlash(roundState.Precommits, blockHash, stakingStore, validatorSet)
}
roundState.ResetRound(roundState.Round + 1)
roundState.LastRoundStart = time.Now()
phase = PhaseProposal
}
roundState.Mu.Unlock()
}
}
}

View File

@ -0,0 +1,23 @@
package simple
import (
"dejo_node/internal/storage"
"dejo_node/internal/transactions"
"fmt"
)
// FinalityManager lida com a finalização de blocos.
type FinalityManager struct {
Storage storage.Storage
}
// NewFinalityManager cria uma nova instância de FinalityManager.
func NewFinalityManager(storage storage.Storage) *FinalityManager {
return &FinalityManager{Storage: storage}
}
// FinalizeBlock salva um bloco finalizado no storage.
func (fm *FinalityManager) FinalizeBlock(b *transactions.Block) error {
fmt.Println("📦 Finalizando bloco:", b.Hash)
return fm.Storage.SaveBlock(b)
}

View File

@ -0,0 +1,69 @@
package simple
import (
"dejo_node/internal/consensus"
"dejo_node/internal/transactions"
"errors"
"fmt"
"go.uber.org/zap"
)
// SimpleEngine é uma implementação básica do mecanismo de consenso.
type SimpleEngine struct {
logger *zap.SugaredLogger
}
// New cria uma nova instância do SimpleEngine
func New() consensus.Engine {
logger, _ := zap.NewProduction()
return &SimpleEngine{
logger: logger.Sugar().Named("consensus.simple"),
}
}
// CanPropose retorna true para permitir que qualquer nó proponha blocos.
func (s *SimpleEngine) CanPropose() bool {
s.logger.Debug("verificando permissão para propor bloco: permitido")
return true
}
// Finalize aplica validações de integridade ao bloco antes de ser aceito.
func (s *SimpleEngine) Finalize(block *transactions.Block) error {
if block == nil {
s.logger.Error("bloco recebido é nulo")
return errors.New("bloco nulo")
}
s.logger.Infow("finalizando bloco",
"index", block.Index,
"txns", len(block.Txns),
"hash", block.Hash,
)
if len(block.Txns) == 0 {
s.logger.Warn("bloco sem transações")
return fmt.Errorf("bloco sem transações não é permitido")
}
if block.Timestamp == 0 {
s.logger.Warn("timestamp ausente")
return fmt.Errorf("timestamp ausente no bloco")
}
hashRecalculado := block.CalculateHash()
if block.Hash != hashRecalculado {
s.logger.Errorw("hash inconsistente",
"esperado", hashRecalculado,
"recebido", block.Hash,
)
return fmt.Errorf("hash inconsistente: esperado %s, calculado %s", block.Hash, hashRecalculado)
}
s.logger.Infow("bloco finalizado com sucesso", "index", block.Index)
return nil
}
// Name retorna o identificador deste mecanismo de consenso.
func (s *SimpleEngine) Name() string {
return "SimpleEngine"
}

View File

@ -0,0 +1,28 @@
package consensus
import (
"dejo_node/internal/staking"
"log"
)
const SlashAmount = 50 // tokens penalizados
// ApplySlash penaliza validadores que não votaram corretamente.
func ApplySlash(precommits map[string]string, blockHash string, stakingStore *staking.StakingStore, validatorSet *ValidatorSet) {
for _, val := range validatorSet.Validators {
vote, voted := precommits[val.Address]
if !voted || vote != blockHash {
// Slashing
info, exists := stakingStore.GetStakeInfo(val.Address)
if exists && info.Amount >= SlashAmount {
newInfo := staking.StakeInfo{
Amount: info.Amount - SlashAmount,
Duration: uint64(info.Duration),
}
stakingStore.UpdateStakeInfo(val.Address, newInfo)
log.Printf("⚡ SLASH: Validador %s penalizado em %d tokens\n", val.Address, SlashAmount)
}
}
}
_ = stakingStore.SaveToDisk("data/staking.db")
}

View File

@ -0,0 +1,42 @@
package consensus
import (
"sync"
"time"
)
// RoundState mantém o estado atual da altura e rodada de consenso.
type RoundState struct {
Height uint64 // Altura atual do consenso (número do bloco)
Round uint64 // Rodada atual (tentativas por altura)
LockedBlock string // Hash do bloco "travado" (caso tenha precommit anterior)
Proposal string // Hash da proposta atual recebida
Prevotes map[string]string // Mapa[ValidatorID] = BlockHash (pode ser vazio)
Precommits map[string]string // Mapa[ValidatorID] = BlockHash
LastRoundStart time.Time // 🆕 Controle de início da rodada
Mu sync.RWMutex // Proteção de acesso concorrente
}
// NewRoundState cria um estado novo para uma altura específica.
func NewRoundState(height uint64) *RoundState {
return &RoundState{
Height: height,
Round: 0,
LockedBlock: "",
Proposal: "",
Prevotes: make(map[string]string),
Precommits: make(map[string]string),
LastRoundStart: time.Now(),
}
}
// ResetRound limpa os votos e proposta da rodada atual (usado ao iniciar nova rodada).
func (rs *RoundState) ResetRound(round uint64) {
rs.Mu.Lock()
defer rs.Mu.Unlock()
rs.Round = round
rs.Proposal = ""
rs.Prevotes = make(map[string]string)
rs.Precommits = make(map[string]string)
rs.LastRoundStart = time.Now()
}

View File

@ -0,0 +1,109 @@
package consensus
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
type HTTPTransport struct {
peers []string
handlers map[MessageType]func(ConsensusMessage)
}
func NewHTTPTransport() *HTTPTransport {
peersEnv := os.Getenv("DEJO_PEERS")
peers := []string{}
if peersEnv != "" {
peers = strings.Split(peersEnv, ",")
}
return &HTTPTransport{
peers: peers,
handlers: make(map[MessageType]func(ConsensusMessage)),
}
}
func (t *HTTPTransport) Broadcast(msg ConsensusMessage) {
data, err := json.Marshal(msg)
if err != nil {
log.Println("❌ Erro ao serializar mensagem para broadcast:", err)
return
}
for _, peer := range t.peers {
go func(peer string) {
resp, err := http.Post(peer+"/consensus", "application/json", bytes.NewReader(data))
if err != nil {
log.Println("⚠️ Erro ao enviar mensagem para", peer, "erro:", err)
return
}
resp.Body.Close()
}(peer)
}
}
func (t *HTTPTransport) Register(msgType MessageType, handler func(ConsensusMessage)) {
t.handlers[msgType] = handler
}
func (t *HTTPTransport) HandleIncoming(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
log.Println("❌ Erro ao ler corpo da mensagem:", err)
w.WriteHeader(http.StatusBadRequest)
return
}
var base BaseMsg
if err := json.Unmarshal(bodyBytes, &base); err != nil {
log.Println("❌ Erro ao decodificar base da mensagem:", err)
w.WriteHeader(http.StatusBadRequest)
return
}
switch base.MsgType {
case ProposalType:
var msg ProposalMsg
_ = json.Unmarshal(bodyBytes, &msg)
if h, ok := t.handlers[ProposalType]; ok {
h(msg)
}
case PrevoteType:
var msg PrevoteMsg
_ = json.Unmarshal(bodyBytes, &msg)
if h, ok := t.handlers[PrevoteType]; ok {
h(msg)
}
case PrecommitType:
var msg PrecommitMsg
_ = json.Unmarshal(bodyBytes, &msg)
if h, ok := t.handlers[PrecommitType]; ok {
h(msg)
}
default:
log.Println("⚠️ Tipo de mensagem desconhecido:", base.MsgType)
}
w.WriteHeader(http.StatusOK)
}
// PingPeer envia um ping para o peer e espera resposta.
func (t *HTTPTransport) PingPeer(peer string) error {
client := http.Client{
Timeout: 2 * time.Second,
}
resp, err := client.Get(peer + "/ping")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("resposta inválida ao ping")
}
return nil
}

View File

@ -0,0 +1,64 @@
package consensus
import (
"dejo_node/internal/staking"
"sort"
)
type Validator struct {
Address string
Stake uint64
}
type ValidatorSet struct {
Validators []Validator
IndexMap map[string]int
}
func NewValidatorSetFromStaking(store *staking.StakingStore, minStake uint64) *ValidatorSet {
vals := []Validator{}
storeSnapshot := store.Snapshot()
for addr, entry := range storeSnapshot {
if entry.Amount >= minStake {
vals = append(vals, Validator{Address: addr, Stake: entry.Amount})
}
}
sort.Slice(vals, func(i, j int) bool {
return vals[i].Stake > vals[j].Stake
})
idx := make(map[string]int)
for i, v := range vals {
idx[v.Address] = i
}
return &ValidatorSet{Validators: vals, IndexMap: idx}
}
func (vs *ValidatorSet) SelectProposer(height uint64) *Validator {
if len(vs.Validators) == 0 {
return nil
}
index := int(height % uint64(len(vs.Validators)))
return &vs.Validators[index]
}
func (vs *ValidatorSet) IsValidator(address string) bool {
_, ok := vs.IndexMap[address]
return ok
}
func (vs *ValidatorSet) TotalStake() uint64 {
sum := uint64(0)
for _, v := range vs.Validators {
sum += v.Stake
}
return sum
}
func (vs *ValidatorSet) ValidatorByAddress(addr string) (Validator, bool) {
i, ok := vs.IndexMap[addr]
if !ok {
return Validator{}, false
}
return vs.Validators[i], true
}

21
internal/crypto/crypto.go Normal file
View File

@ -0,0 +1,21 @@
package crypto
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
)
// GenerateHash gera um hash SHA-256 de qualquer estrutura
func GenerateHash(v interface{}) string {
b, _ := json.Marshal(v)
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
// SignMessage assina uma string com uma chave privada
func SignMessage(message string, privateKey ed25519.PrivateKey) string {
sig := ed25519.Sign(privateKey, []byte(message))
return hex.EncodeToString(sig)
}

7
internal/crypto/keys.go Normal file
View File

@ -0,0 +1,7 @@
package crypto
import "crypto/ecdsa"
// GlobalPrivateKey é usada temporariamente como referência global para testes e simulações.
// Em produção, isso deve ser gerenciado de forma segura.
var GlobalPrivateKey *ecdsa.PrivateKey

View File

@ -0,0 +1,26 @@
package crypto
import "fmt"
// PQSigner é um placeholder para um algoritmo de assinatura pós-quântico (ex: Dilithium, Falcon)
type PQSigner struct{}
func (s *PQSigner) GenerateKeys() (string, string, error) {
return "", "", fmt.Errorf("PQSigner ainda não implementado")
}
func (s *PQSigner) Sign(message, privKey string) (string, error) {
return "", fmt.Errorf("PQSigner ainda não implementado")
}
func (s *PQSigner) Verify(message, signature, pubKey string) bool {
return false
}
func (s *PQSigner) Name() string {
return "pq"
}
func init() {
Register(&PQSigner{})
}

27
internal/crypto/signer.go Normal file
View File

@ -0,0 +1,27 @@
package crypto
import "errors"
// Signer representa qualquer esquema de assinatura digital
// (ex: ECDSA, pós-quântico, etc)
type Signer interface {
GenerateKeys() (privKey string, pubKey string, err error)
Sign(message, privKey string) (string, error)
Verify(message, signature, pubKey string) bool
Name() string
}
var registered = make(map[string]Signer)
// Register um novo esquema de assinatura
func Register(signer Signer) {
registered[signer.Name()] = signer
}
// Get retorna o esquema de assinatura por nome
func Get(name string) (Signer, error) {
if s, ok := registered[name]; ok {
return s, nil
}
return nil, errors.New("assinador não encontrado")
}

104
internal/dao/proposal.go Normal file
View File

@ -0,0 +1,104 @@
package dao
import (
"encoding/json"
"log"
"sync"
"time"
)
type ProposalType string
const (
GenericProposal ProposalType = "GENERIC"
ParamChangeProposal ProposalType = "PARAM_CHANGE"
)
type Proposal struct {
ID uint64
Title string
Content string
Type ProposalType
Creator string
CreatedAt int64
Duration int64 // segundos até expiração
Votes map[string]bool
Closed bool
Approved bool
mu sync.Mutex
}
func NewProposal(id uint64, title, content, creator string, typ ProposalType, duration int64) *Proposal {
return &Proposal{
ID: id,
Title: title,
Content: content,
Type: typ,
Creator: creator,
CreatedAt: time.Now().Unix(),
Duration: duration,
Votes: make(map[string]bool),
Closed: false,
Approved: false,
}
}
func (p *Proposal) Vote(address string, approve bool) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.Closed {
return nil
}
p.Votes[address] = approve
return nil
}
func (p *Proposal) Tally(stakes map[string]uint64) (yes uint64, no uint64, total uint64) {
p.mu.Lock()
defer p.mu.Unlock()
for addr, vote := range p.Votes {
stake := stakes[addr]
total += stake
if vote {
yes += stake
} else {
no += stake
}
}
return
}
func (p *Proposal) CheckAndClose(stakes map[string]uint64) {
if p.Closed {
return
}
if time.Now().Unix() >= p.CreatedAt+p.Duration {
yes, _, total := p.Tally(stakes)
if yes*100/total > 66 {
p.Approved = true
}
p.Closed = true
if p.Approved {
p.Execute()
}
}
}
// Execute aplica os efeitos da proposta, se for do tipo que altera estado
func (p *Proposal) Execute() {
if p.Type == ParamChangeProposal {
var payload struct {
Target string `json:"target"`
Value uint64 `json:"value"`
}
err := json.Unmarshal([]byte(p.Content), &payload)
if err != nil {
log.Printf("❌ Erro ao executar proposta PARAM_CHANGE: %v", err)
return
}
// Exemplo: muda parâmetro global (placeholder)
log.Printf("⚙️ Aplicando PARAM_CHANGE: %s = %d", payload.Target, payload.Value)
// Aqui você aplicaria no config real
}
}

78
internal/dao/store.go Normal file
View File

@ -0,0 +1,78 @@
package dao
import (
"encoding/gob"
"errors"
"os"
"sync"
)
type ProposalStore struct {
mu sync.RWMutex
proposals map[uint64]*Proposal
nextID uint64
}
func NewProposalStore() *ProposalStore {
return &ProposalStore{
proposals: make(map[uint64]*Proposal),
nextID: 1,
}
}
func (ps *ProposalStore) Create(title, content, creator string, typ ProposalType, duration int64) *Proposal {
ps.mu.Lock()
defer ps.mu.Unlock()
id := ps.nextID
ps.nextID++
p := NewProposal(id, title, content, creator, typ, duration)
ps.proposals[id] = p
return p
}
func (ps *ProposalStore) Get(id uint64) (*Proposal, error) {
ps.mu.RLock()
defer ps.mu.RUnlock()
p, ok := ps.proposals[id]
if !ok {
return nil, errors.New("proposta não encontrada")
}
return p, nil
}
func (ps *ProposalStore) List() []*Proposal {
ps.mu.RLock()
defer ps.mu.RUnlock()
var list []*Proposal
for _, p := range ps.proposals {
list = append(list, p)
}
return list
}
func (ps *ProposalStore) SaveToDisk(path string) error {
ps.mu.RLock()
defer ps.mu.RUnlock()
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := gob.NewEncoder(f)
return enc.Encode(ps)
}
func (ps *ProposalStore) LoadFromDisk(path string) error {
ps.mu.Lock()
defer ps.mu.Unlock()
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer f.Close()
dec := gob.NewDecoder(f)
return dec.Decode(ps)
}

View File

@ -0,0 +1,25 @@
package mempool
import "dejo_node/internal/transactions"
type Mempool struct {
transactions []*transactions.Transaction
}
func NewMempool() *Mempool {
return &Mempool{
transactions: []*transactions.Transaction{},
}
}
func (m *Mempool) Add(tx *transactions.Transaction) {
m.transactions = append(m.transactions, tx)
}
func (m *Mempool) All() []*transactions.Transaction {
return m.transactions
}
func (m *Mempool) Clear() {
m.transactions = []*transactions.Transaction{}
}

76
internal/miner/miner.go Normal file
View File

@ -0,0 +1,76 @@
package miner
import (
"dejo_node/internal/consensus"
"dejo_node/internal/storage"
"dejo_node/internal/transactions"
"dejo_node/internal/ws"
"fmt"
"time"
)
// Miner representa o componente responsável por propor blocos válidos.
type Miner struct {
Engine consensus.Engine
BlockStore *storage.BlockStore
Mempool *transactions.Mempool
}
// New cria uma nova instância de minerador
func New(engine consensus.Engine, store *storage.BlockStore, mempool *transactions.Mempool) *Miner {
return &Miner{
Engine: engine,
BlockStore: store,
Mempool: mempool,
}
}
// MineNextBlock propõe, valida e armazena um novo bloco se possível
func (m *Miner) MineNextBlock() (*transactions.Block, error) {
if !m.Engine.CanPropose() {
return nil, fmt.Errorf("este nó não tem permissão para propor blocos")
}
txns := m.Mempool.Pending()
if len(txns) == 0 {
return nil, fmt.Errorf("nenhuma transação pendente para minerar")
}
lastBlock, err := m.BlockStore.GetLatestBlock()
index := uint64(0)
prevHash := ""
if err == nil {
index = lastBlock.Index + 1
prevHash = lastBlock.Hash
}
blk := &transactions.Block{
Index: index,
Timestamp: time.Now().Unix(),
PrevHash: prevHash,
Txns: txns,
}
blk.ComputeHash()
if err := m.Engine.Finalize(blk); err != nil {
return nil, fmt.Errorf("bloco rejeitado pelo consenso: %w", err)
}
if err := m.BlockStore.SaveBlock(blk); err != nil {
return nil, fmt.Errorf("erro ao salvar bloco: %w", err)
}
// Emitir eventos WebSocket
ws.Emit("newBlock", map[string]any{
"index": blk.Index,
"hash": blk.Hash,
})
for _, tx := range blk.Txns {
ws.Emit("txConfirmed", map[string]any{
"hash": tx.Hash(),
})
}
m.Mempool.Clear()
return blk, nil
}

View File

@ -0,0 +1,57 @@
package monitor
import (
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
var (
peers = make(map[string]bool)
peersLock sync.RWMutex
heartbeatFreq = 3 * time.Second
)
func StartHeartbeatMonitor() {
peerList := strings.Split(os.Getenv("DEJO_PEERS"), ",")
for _, p := range peerList {
peers[p] = false // Inicialmente offline
}
go func() {
ticker := time.NewTicker(heartbeatFreq)
for range ticker.C {
checkPeers(peerList)
}
}()
}
func checkPeers(peerList []string) {
for _, peer := range peerList {
go func(p string) {
resp, err := http.Get(p + "/ping")
peersLock.Lock()
defer peersLock.Unlock()
if err != nil || resp.StatusCode != http.StatusOK {
if peers[p] {
log.Printf("⚠️ Peer %s está OFFLINE", p)
}
peers[p] = false
} else {
if !peers[p] {
log.Printf("✅ Peer %s voltou ONLINE", p)
}
peers[p] = true
}
} (peer)
}
}
func IsPeerOnline(peer string) bool {
peersLock.RLock()
defer peersLock.RUnlock()
return peers[peer]
}

View File

@ -0,0 +1,43 @@
package monitor
import (
"sync"
"time"
)
type Monitor struct {
peers map[string]bool
mutex sync.RWMutex
heartbeatFn func(peer string) bool
}
func NewMonitor(heartbeatFn func(peer string) bool) *Monitor {
return &Monitor{
peers: make(map[string]bool),
heartbeatFn: heartbeatFn,
}
}
func (m *Monitor) Start(peers []string) {
for _, peer := range peers {
go func(p string) {
for {
m.updateStatus(p)
time.Sleep(5 * time.Second)
}
}(peer)
}
}
func (m *Monitor) updateStatus(peer string) {
alive := m.heartbeatFn(peer)
m.mutex.Lock()
defer m.mutex.Unlock()
m.peers[peer] = alive
}
func (m *Monitor) IsPeerOnline(peer string) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.peers[peer]
}

View File

@ -0,0 +1,61 @@
package monitoring
import (
"log"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
txCounter = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "dejo_tx_total",
Help: "Total de transações recebidas pelo nó",
},
)
finalizedBlocks = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "dejo_finalized_blocks_total",
Help: "Total de blocos finalizados pelo nó",
},
)
uptimeGauge = prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Name: "dejo_uptime_seconds",
Help: "Tempo desde que o nó foi iniciado",
},
func() float64 {
return time.Since(startTime).Seconds()
},
)
)
var startTime = time.Now()
// Init inicializa o endpoint /metrics
func Init() {
prometheus.MustRegister(txCounter)
prometheus.MustRegister(finalizedBlocks)
prometheus.MustRegister(uptimeGauge)
go func() {
http.Handle("/metrics", promhttp.Handler())
log.Println("📊 Endpoint de métricas disponível em /metrics")
http.ListenAndServe(":9100", nil)
}()
}
// IncTxCount incrementa o total de transações recebidas
func IncTxCount() {
txCounter.Inc()
}
// IncFinalizedBlocks incrementa o total de blocos finalizados
func IncFinalizedBlocks() {
finalizedBlocks.Inc()
}

View File

@ -0,0 +1,59 @@
package oracle
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
// OracleData representa um dado externo coletado via oráculo
type OracleData struct {
Source string `json:"source"`
Key string `json:"key"`
Value float64 `json:"value"`
Time time.Time `json:"timestamp"`
}
// FetchFromMock simula obtenção de dados de um oráculo externo (mock)
func FetchFromMock(key string) (*OracleData, error) {
mockPrices := map[string]float64{
"USD-BRL": 5.06,
"ETH-BRL": 18732.42,
"IMOVEL-ABC123": 950000.00,
}
val, ok := mockPrices[key]
if !ok {
return nil, errors.New("dado não encontrado no mock")
}
return &OracleData{
Source: "mock",
Key: key,
Value: val,
Time: time.Now(),
}, nil
}
// FetchFromCoinGecko busca o preço de uma moeda em relação ao BRL
func FetchFromCoinGecko(coin string) (*OracleData, error) {
url := fmt.Sprintf("https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=brl", coin)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var parsed map[string]map[string]float64
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
price := parsed[coin]["brl"]
return &OracleData{
Source: "coingecko",
Key: coin + "-BRL",
Value: price,
Time: time.Now(),
}, nil
}

View File

@ -0,0 +1,20 @@
package oracle
import (
"testing"
)
func TestFetchFromMock(t *testing.T) {
d, err := FetchFromMock("USD-BRL")
if err != nil {
t.Fatalf("Erro ao buscar mock: %v", err)
}
if d.Key != "USD-BRL" || d.Value <= 0 {
t.Errorf("Valor inesperado do mock: %+v", d)
}
_, err = FetchFromMock("INVALIDO")
if err == nil {
t.Error("Esperado erro ao buscar chave inválida no mock")
}
}

49
internal/oracle/store.go Normal file
View File

@ -0,0 +1,49 @@
package oracle
import (
"sync"
)
// Store mantém os últimos dados válidos dos oráculos e um pequeno histórico
type Store struct {
mu sync.RWMutex
latest map[string]*OracleData
history map[string][]*OracleData
maxHist int
}
// NewStore cria uma nova instância de armazenamento de oráculos
func NewStore(maxHistory int) *Store {
return &Store{
latest: make(map[string]*OracleData),
history: make(map[string][]*OracleData),
maxHist: maxHistory,
}
}
// Save registra um novo dado e atualiza o histórico
func (s *Store) Save(data *OracleData) {
s.mu.Lock()
defer s.mu.Unlock()
s.latest[data.Key] = data
s.history[data.Key] = append(s.history[data.Key], data)
if len(s.history[data.Key]) > s.maxHist {
s.history[data.Key] = s.history[data.Key][len(s.history[data.Key])-s.maxHist:]
}
}
// Get retorna o último dado registrado
func (s *Store) Get(key string) *OracleData {
s.mu.RLock()
defer s.mu.RUnlock()
return s.latest[key]
}
// History retorna o histórico completo de um dado
func (s *Store) History(key string) []*OracleData {
s.mu.RLock()
defer s.mu.RUnlock()
return s.history[key]
}

View File

@ -0,0 +1,30 @@
package oracle
import "testing"
func TestStore_SaveAndGet(t *testing.T) {
s := NewStore(3)
d := &OracleData{Key: "ETH-BRL", Value: 10000.0}
s.Save(d)
if got := s.Get("ETH-BRL"); got == nil || got.Value != 10000.0 {
t.Error("falha ao buscar valor salvo")
}
}
func TestStore_History(t *testing.T) {
s := NewStore(2)
s.Save(&OracleData{Key: "ETH-BRL", Value: 1})
s.Save(&OracleData{Key: "ETH-BRL", Value: 2})
s.Save(&OracleData{Key: "ETH-BRL", Value: 3}) // deve descartar o primeiro
hist := s.History("ETH-BRL")
if len(hist) != 2 {
t.Errorf("esperado 2 entradas no histórico, obtido %d", len(hist))
}
if hist[0].Value != 2 || hist[1].Value != 3 {
t.Error("histórico não está correto")
}
}

View File

@ -0,0 +1,45 @@
package oracle
import (
"fmt"
"math"
"time"
)
// ValidateMajority recebe múltiplas leituras e tenta achar um consenso baseado na maioria simples.
func ValidateMajority(data []*OracleData, tolerance float64) (*OracleData, error) {
if len(data) == 0 {
return nil, fmt.Errorf("nenhum dado recebido para validar")
}
count := make(map[float64]int)
latest := make(map[float64]time.Time)
for _, d := range data {
rounded := math.Round(d.Value/tolerance) * tolerance // agrupar valores similares
count[rounded]++
if d.Time.After(latest[rounded]) {
latest[rounded] = d.Time
}
}
// Encontrar o valor com mais votos
var maxVal float64
var maxCount int
for val, c := range count {
if c > maxCount {
maxVal = val
maxCount = c
}
}
// Retornar o OracleData mais recente com esse valor
for _, d := range data {
rounded := math.Round(d.Value/tolerance) * tolerance
if rounded == maxVal && d.Time.Equal(latest[maxVal]) {
return d, nil
}
}
return nil, fmt.Errorf("nenhum consenso válido encontrado")
}

View File

@ -0,0 +1,28 @@
package oracle
import (
"testing"
"time"
)
func TestValidateMajority(t *testing.T) {
now := time.Now()
d1 := &OracleData{Key: "USD-BRL", Value: 5.00, Time: now}
d2 := &OracleData{Key: "USD-BRL", Value: 5.01, Time: now.Add(-1 * time.Minute)}
d3 := &OracleData{Key: "USD-BRL", Value: 4.99, Time: now.Add(-2 * time.Minute)}
res, err := ValidateMajority([]*OracleData{d1, d2, d3}, 0.05)
if err != nil {
t.Fatalf("Falha ao validar maioria: %v", err)
}
if res == nil || res.Value == 0 {
t.Error("Valor inválido retornado pelo consenso")
}
// sem consenso
d4 := &OracleData{Key: "USD-BRL", Value: 3.0, Time: now}
_, err = ValidateMajority([]*OracleData{d1, d4}, 0.001)
if err == nil {
t.Error("Esperado erro por falta de consenso")
}
}

52
internal/p2p/dht.go Normal file
View File

@ -0,0 +1,52 @@
package p2p
import (
"context"
"fmt"
"time"
dht "github.com/libp2p/go-libp2p-kad-dht"
routingdiscovery "github.com/libp2p/go-libp2p/p2p/discovery/routing"
)
const rendezvousString = "dejo-global"
// InitDHT inicializa o Kademlia DHT e ativa descoberta global de peers.
func (p *P2PNode) InitDHT(ctx context.Context) error {
d, err := dht.New(ctx, p.Host)
if err != nil {
return fmt.Errorf("erro ao iniciar DHT: %w", err)
}
if err := d.Bootstrap(ctx); err != nil {
return fmt.Errorf("erro ao bootstrap DHT: %w", err)
}
routing := routingdiscovery.NewRoutingDiscovery(d)
_, err = routing.Advertise(ctx, rendezvousString)
if err != nil {
return fmt.Errorf("erro ao anunciar no DHT: %w", err)
}
fmt.Println("📡 Anunciado no DHT com tag:", rendezvousString)
go func() {
for {
peers, err := routing.FindPeers(ctx, rendezvousString)
if err != nil {
fmt.Println("Erro ao buscar peers:", err)
continue
}
for pinfo := range peers {
if pinfo.ID == p.Host.ID() {
continue // ignora si mesmo
}
fmt.Println("🌍 Peer encontrado via DHT:", pinfo.ID)
_ = p.Host.Connect(ctx, pinfo)
}
time.Sleep(10 * time.Second)
}
}()
return nil
}

66
internal/p2p/host.go Normal file
View File

@ -0,0 +1,66 @@
package p2p
import (
"crypto/rand"
"fmt"
"log"
"github.com/libp2p/go-libp2p"
crypto "github.com/libp2p/go-libp2p/core/crypto"
hostlib "github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
ma "github.com/multiformats/go-multiaddr"
)
// NewSecureHost cria um nó P2P com proteção básica anti-Sybil/DDoS
func NewSecureHost(port int) (hostlib.Host, error) {
// Gera par de chaves (identidade)
priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 2048, rand.Reader)
if err != nil {
return nil, fmt.Errorf("falha ao gerar chave: %v", err)
}
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("erro ao obter PeerID: %v", err)
}
addr, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))
h, err := libp2p.New(
libp2p.ListenAddrs(addr),
libp2p.Identity(priv),
)
if err != nil {
return nil, fmt.Errorf("erro ao criar host libp2p: %v", err)
}
log.Printf("✅ Host P2P criado: %s (%s)\n", pid.String(), addr)
// Proteção: loga e valida conexões
h.Network().Notify(&connLogger{})
return h, nil
}
// connLogger registra e controla conexões suspeitas
type connLogger struct{}
func (c *connLogger) Connected(n network.Network, conn network.Conn) {
if err := LimitConnections(conn); err != nil {
log.Printf("🚫 Conexão rejeitada: %s (%v)", conn.RemotePeer(), err)
conn.Close()
return
}
log.Printf("🔗 Peer conectado: %s (%s)", conn.RemotePeer(), conn.RemoteMultiaddr())
}
func (c *connLogger) Disconnected(n network.Network, conn network.Conn) {
log.Printf("❌ Peer desconectado: %s", conn.RemotePeer())
ClearConnection(conn)
}
func (c *connLogger) OpenedStream(n network.Network, s network.Stream) {}
func (c *connLogger) ClosedStream(n network.Network, s network.Stream) {}
func (c *connLogger) Listen(n network.Network, addr ma.Multiaddr) {}
func (c *connLogger) ListenClose(n network.Network, addr ma.Multiaddr) {}

58
internal/p2p/limiter.go Normal file
View File

@ -0,0 +1,58 @@
package p2p
import (
"net"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/network"
)
const (
MaxConnsPerIP = 5
MinReconnectGap = 10 * time.Second
)
var (
ipConnCount = make(map[string]int)
peerLastSeen = make(map[string]time.Time)
limiterMu sync.Mutex
)
// LimitConnections implementa proteção básica anti-DDoS/Sybil
func LimitConnections(conn network.Conn) error {
limiterMu.Lock()
defer limiterMu.Unlock()
ip, _, err := net.SplitHostPort(conn.RemoteMultiaddr().String())
if err != nil {
return nil // fallback: não bloqueia
}
ipConnCount[ip]++
if ipConnCount[ip] > MaxConnsPerIP {
return network.ErrReset
}
peerID := conn.RemotePeer().String()
last := peerLastSeen[peerID]
if time.Since(last) < MinReconnectGap {
return network.ErrReset
}
peerLastSeen[peerID] = time.Now()
return nil
}
// ClearConnection cleanup quando o peer desconecta
func ClearConnection(conn network.Conn) {
limiterMu.Lock()
defer limiterMu.Unlock()
ip, _, err := net.SplitHostPort(conn.RemoteMultiaddr().String())
if err == nil {
ipConnCount[ip]--
if ipConnCount[ip] <= 0 {
delete(ipConnCount, ip)
}
}
}

16
internal/p2p/network.go Normal file
View File

@ -0,0 +1,16 @@
package p2p
import (
"log"
)
// StartNetwork inicializa a rede P2P com host seguro
func StartNetwork(port int) {
h, err := NewSecureHost(port)
if err != nil {
log.Fatalf("Erro ao iniciar host P2P: %v", err)
}
log.Printf("🌐 Rede P2P inicializada com sucesso: PeerID %s\n", h.ID())
select {} // Mantém processo vivo
}

81
internal/p2p/p2p.go Normal file
View File

@ -0,0 +1,81 @@
package p2p
import (
"context"
"crypto/rand"
"fmt"
host "github.com/libp2p/go-libp2p/core/host"
libp2p "github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
)
const ProtocolID = "/dejo/1.0.0"
// P2PNode representa um nó da rede P2P da DEJO.
type P2PNode struct {
Host host.Host
PeerChan chan peer.AddrInfo
}
// NewP2PNode cria um novo nó libp2p.
func NewP2PNode(ctx context.Context) (*P2PNode, error) {
priv, _, _ := crypto.GenerateKeyPairWithReader(crypto.Ed25519, -1, rand.Reader)
h, err := libp2p.New(
libp2p.Identity(priv),
libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"),
)
if err != nil {
return nil, err
}
node := &P2PNode{
Host: h,
PeerChan: make(chan peer.AddrInfo),
}
// Handler para conexões recebidas
h.SetStreamHandler(ProtocolID, func(s network.Stream) {
buf := make([]byte, 1024)
s.Read(buf)
fmt.Printf("📡 Recebido: %s\n", string(buf))
s.Close()
})
return node, nil
}
// HandlePeerFound é chamado quando um novo peer é descoberto.
func (p *P2PNode) HandlePeerFound(pi peer.AddrInfo) {
fmt.Println("👋 Novo peer descoberto:", pi.ID)
p.PeerChan <- pi
}
// ConnectToPeers tenta se conectar aos peers encontrados.
func (p *P2PNode) ConnectToPeers(ctx context.Context) {
go func() {
for pi := range p.PeerChan {
if err := p.Host.Connect(ctx, pi); err != nil {
fmt.Println("Erro ao conectar ao peer:", err)
} else {
fmt.Println("🔗 Conectado ao peer:", pi.ID)
}
}
}()
}
// Broadcast envia uma mensagem para todos os peers conectados.
func (p *P2PNode) Broadcast(msg string) {
for _, c := range p.Host.Network().Peers() {
stream, err := p.Host.NewStream(context.Background(), c, ProtocolID)
if err != nil {
fmt.Println("Erro ao abrir stream:", err)
continue
}
stream.Write([]byte(msg))
stream.Close()
}
}

35
internal/p2p/p2p_test.go Normal file
View File

@ -0,0 +1,35 @@
package p2p_test
import (
"context"
"dejo_node/internal/p2p"
"testing"
"time"
)
func TestP2PNode_DHT(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
nodeA, err := p2p.NewP2PNode(ctx)
if err != nil {
t.Fatalf("falha ao criar node A: %v", err)
}
nodeA.ConnectToPeers(ctx)
if err := nodeA.InitDHT(ctx); err != nil {
t.Fatalf("erro ao iniciar DHT no node A: %v", err)
}
nodeB, err := p2p.NewP2PNode(ctx)
if err != nil {
t.Fatalf("falha ao criar node B: %v", err)
}
nodeB.ConnectToPeers(ctx)
if err := nodeB.InitDHT(ctx); err != nil {
t.Fatalf("erro ao iniciar DHT no node B: %v", err)
}
time.Sleep(5 * time.Second)
nodeB.Broadcast("msg: test from B to A")
time.Sleep(5 * time.Second)
}

View File

@ -0,0 +1,19 @@
package staking
import (
"math"
"time"
)
// CalculateRewards calcula as recompensas baseadas em tempo e APR.
// Exemplo: APR 12% ao ano = 0.01 ao mês, 0.00033 ao dia.
func CalculateRewards(amount uint64, startTime int64, apr float64) uint64 {
daysStaked := float64(time.Now().Unix()-startTime) / (60 * 60 * 24)
if daysStaked <= 0 {
return 0
}
// Fórmula de juros simples: R = P * i * t
interest := float64(amount) * (apr / 365.0) * daysStaked
return uint64(math.Floor(interest))
}

82
internal/staking/store.go Normal file
View File

@ -0,0 +1,82 @@
package staking
import (
"encoding/gob"
"os"
"sync"
)
type StakeInfo struct {
Amount uint64
Duration uint64
}
type StakingStore struct {
data map[string]StakeInfo
mu sync.RWMutex
}
func NewStakingStore() *StakingStore {
return &StakingStore{
data: make(map[string]StakeInfo),
}
}
func (s *StakingStore) Stake(address string, amount uint64, duration uint64) error {
s.mu.Lock()
defer s.mu.Unlock()
s.data[address] = StakeInfo{Amount: amount, Duration: duration}
return nil
}
func (s *StakingStore) Unstake(address string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.data, address)
return nil
}
func (s *StakingStore) GetStakeInfo(address string) (StakeInfo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
info, ok := s.data[address]
return info, ok
}
func (s *StakingStore) UpdateStakeInfo(address string, info StakeInfo) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[address] = info
}
func (s *StakingStore) Snapshot() map[string]StakeInfo {
s.mu.RLock()
defer s.mu.RUnlock()
clone := make(map[string]StakeInfo)
for k, v := range s.data {
clone[k] = v
}
return clone
}
func (s *StakingStore) SaveToDisk(filename string) error {
s.mu.RLock()
defer s.mu.RUnlock()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
return encoder.Encode(s.data)
}
func (s *StakingStore) LoadFromDisk(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
decoder := gob.NewDecoder(file)
return decoder.Decode(&s.data)
}

8
internal/state/apply.go Normal file
View File

@ -0,0 +1,8 @@
package state
import (
"dejo_node/internal/staking"
)
var GlobalState = NewState()
var GlobalStakingStore = staking.NewStakingStore()

7
internal/state/domain.go Normal file
View File

@ -0,0 +1,7 @@
package state
func (s *State) GetBalance(addr string) uint64 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.Balances[addr]
}

99
internal/state/state.go Normal file
View File

@ -0,0 +1,99 @@
package state
import (
"encoding/gob"
"fmt"
"os"
"sync"
"dejo_node/internal/staking"
"dejo_node/internal/transactions"
)
type State struct {
mu sync.RWMutex
Balances map[string]uint64
}
func NewState() *State {
return &State{
Balances: make(map[string]uint64),
}
}
func (s *State) Apply(tx *transactions.Transaction) error {
s.mu.Lock()
defer s.mu.Unlock()
if tx.From == tx.To {
return fmt.Errorf("transação inválida: origem = destino")
}
if tx.Value <= 0 {
return fmt.Errorf("valor zero não permitido")
}
fromBal := s.Balances[tx.From]
if fromBal < uint64(tx.Value) {
return fmt.Errorf("saldo insuficiente: %s tem %d, precisa de %f", tx.From, fromBal, tx.Value)
}
s.Balances[tx.From] -= uint64(tx.Value)
s.Balances[tx.To] += uint64(tx.Value)
return nil
}
func (s *State) Transfer(from, to string, amount uint64) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.Balances[from] < amount {
return fmt.Errorf("saldo insuficiente")
}
s.Balances[from] -= amount
s.Balances[to] += amount
return nil
}
func (s *State) Mint(to string, amount uint64) {
s.mu.Lock()
defer s.mu.Unlock()
s.Balances[to] += amount
}
func (s *State) Stake(address string, amount uint64, dur int64) error {
if err := s.Transfer(address, "staking_pool", amount); err != nil {
return err
}
return staking.NewStakingStore().Stake(address, amount, uint64(dur))
}
func (s *State) Unstake(address string) error {
s.mu.Lock()
defer s.mu.Unlock()
if err := staking.NewStakingStore().Unstake(address); err != nil {
return err
}
s.Balances[address] += 1000 // Exemplo: devolve saldo fixo
return nil
}
func (s *State) SaveToDisk(path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := gob.NewEncoder(f)
return enc.Encode(s.Balances)
}
func (s *State) LoadFromDisk(path string) error {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
s.Balances = make(map[string]uint64)
return nil
}
return err
}
defer f.Close()
dec := gob.NewDecoder(f)
return dec.Decode(&s.Balances)
}

View File

@ -0,0 +1,24 @@
package storage
import (
"errors"
"os"
"path/filepath"
"strconv"
)
const snapshotFile = "state_snapshot.txt"
func (bs *BlockStore) SetSnapshotHeight(index uint64) error {
file := filepath.Join(bs.Path, snapshotFile)
return os.WriteFile(file, []byte(strconv.FormatUint(index, 10)), 0644)
}
func (bs *BlockStore) GetSnapshotHeight() (uint64, error) {
file := filepath.Join(bs.Path, snapshotFile)
data, err := os.ReadFile(file)
if err != nil {
return 0, errors.New("nenhum snapshot salvo")
}
return strconv.ParseUint(string(data), 10, 64)
}

View File

@ -0,0 +1,8 @@
package storage
import "dejo_node/internal/transactions"
// Storage define a interface para persistência de blocos.
type Storage interface {
SaveBlock(block *transactions.Block) error
}

86
internal/storage/store.go Normal file
View File

@ -0,0 +1,86 @@
package storage
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"dejo_node/internal/transactions"
)
type BlockStore struct {
Path string
}
func NewBlockStore(path string) *BlockStore {
_ = os.MkdirAll(path, 0755)
return &BlockStore{Path: path}
}
func (s *BlockStore) SaveBlock(block *transactions.Block) error {
file := filepath.Join(s.Path, fmt.Sprintf("block_%03d.json", block.Index))
data, err := json.MarshalIndent(block, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(file, data, 0644)
}
func (s *BlockStore) LoadBlock(height int) (*transactions.Block, error) {
file := filepath.Join(s.Path, fmt.Sprintf("block_%03d.json", height))
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
var block transactions.Block
err = json.Unmarshal(data, &block)
if err != nil {
return nil, err
}
return &block, nil
}
func (s *BlockStore) ListBlocks() ([]*transactions.Block, error) {
files, err := ioutil.ReadDir(s.Path)
if err != nil {
return nil, err
}
var blocks []*transactions.Block
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
continue
}
heightStr := file.Name()[6:9]
height, err := strconv.Atoi(heightStr)
if err != nil {
continue
}
block, err := s.LoadBlock(height)
if err == nil {
blocks = append(blocks, block)
}
}
sort.Slice(blocks, func(i, j int) bool {
return blocks[i].Index < blocks[j].Index
})
return blocks, nil
}
func (s *BlockStore) GetLatestBlock() (*transactions.Block, error) {
blocks, err := s.ListBlocks()
if err != nil || len(blocks) == 0 {
return nil, fmt.Errorf("nenhum bloco disponível")
}
return blocks[len(blocks)-1], nil
}
func (s *BlockStore) GetBlockByIndex(index int) (*transactions.Block, error) {
return s.LoadBlock(index)
}
func (s *BlockStore) LoadAll() ([]*transactions.Block, error) {
return s.ListBlocks()
}

68
internal/sync/height.go Normal file
View File

@ -0,0 +1,68 @@
package sync
import (
"encoding/json"
"fmt"
"io"
"dejo_node/internal/transactions"
network "github.com/libp2p/go-libp2p/core/network"
)
type SyncMessage struct {
Type string `json:"type"`
Height int64 `json:"height,omitempty"`
Block *transactions.Block `json:"block,omitempty"`
}
func (s *SyncManager) handleStream(stream network.Stream) {
fmt.Println("📥 [SYNC] Conexão de:", stream.Conn().RemotePeer())
defer stream.Close()
decoder := json.NewDecoder(stream)
encoder := json.NewEncoder(stream)
for {
var msg SyncMessage
if err := decoder.Decode(&msg); err != nil {
if err == io.EOF {
return
}
fmt.Println("Erro ao decodificar mensagem de sync:", err)
return
}
switch msg.Type {
case "RequestHeight":
latest, err := s.BlockStore.GetLatestBlock()
if err != nil {
fmt.Println("Erro ao obter bloco mais recente:", err)
return
}
height := int64(latest.Index)
resp := SyncMessage{
Type: "ResponseHeight",
Height: height,
}
_ = encoder.Encode(resp)
fmt.Println("↩️ Respondendo altura:", height)
case "RequestBlock":
blk, err := s.BlockStore.GetBlockByIndex(int(msg.Height))
if err != nil {
fmt.Println("❌ Erro ao buscar bloco:", err)
return
}
resp := SyncMessage{
Type: "ResponseBlock",
Block: blk,
}
_ = encoder.Encode(resp)
fmt.Printf("📤 Enviando bloco altura %d\n", msg.Height)
default:
fmt.Println("Mensagem de sync desconhecida:", msg.Type)
}
}
}

33
internal/sync/sync.go Normal file
View File

@ -0,0 +1,33 @@
package sync
import (
"context"
"fmt"
"dejo_node/internal/storage"
host "github.com/libp2p/go-libp2p/core/host"
)
const SyncProtocolID = "/dejo/sync/1.0.0"
// SyncManager coordena o processo de sincronização de blocos com outros peers.
type SyncManager struct {
Ctx context.Context
Host host.Host
BlockStore *storage.BlockStore
}
// NewSyncManager cria um novo gerenciador de sincronização.
func NewSyncManager(ctx context.Context, h host.Host, bs *storage.BlockStore) *SyncManager {
manager := &SyncManager{
Ctx: ctx,
Host: h,
BlockStore: bs,
}
h.SetStreamHandler(SyncProtocolID, manager.handleStream)
fmt.Println("🔄 Handler de sync registrado para protocolo:", SyncProtocolID)
return manager
}

View File

@ -0,0 +1,48 @@
package transactions
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
// Block representa um bloco da blockchain DEJO.
type Block struct {
Index uint64 // posição na cadeia
PrevHash string // hash do bloco anterior
Txns []*Transaction // lista de transações
Timestamp int64 // timestamp unix
Nonce uint64 // reservado para PoW/futuro
Hash string // hash do bloco
}
// CreateBlock monta um novo bloco com transações válidas.
func CreateBlock(prevHash string, txs []*Transaction, index uint64) *Block {
block := &Block{
Index: index,
PrevHash: prevHash,
Txns: txs,
Timestamp: time.Now().Unix(),
Nonce: 0, // reservado para PoW ou consenso
}
block.Hash = block.CalculateHash()
return block
}
// CalculateHash calcula o hash do bloco.
func (b *Block) CalculateHash() string {
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%d:%s:%d:%d",
b.Index, b.PrevHash, b.Timestamp, b.Nonce)))
for _, tx := range b.Txns {
h.Write([]byte(tx.Hash()))
}
return hex.EncodeToString(h.Sum(nil))
}
// ComputeHash atualiza o campo Hash com base nos dados atuais do bloco.
func (b *Block) ComputeHash() {
b.Hash = b.CalculateHash()
}

View File

@ -0,0 +1,41 @@
package transactions_test
import (
"dejo_node/internal/transactions"
"testing"
)
func TestCreateBlock(t *testing.T) {
tx1 := &transactions.Transaction{
From: "A",
To: "B",
Value: 10,
Nonce: 1,
Gas: 1,
Signature: "sig1",
}
tx2 := &transactions.Transaction{
From: "B",
To: "C",
Value: 5,
Nonce: 1,
Gas: 1,
Signature: "sig2",
}
block := transactions.CreateBlock("prevhash", []*transactions.Transaction{tx1, tx2}, 2)
if block.Hash == "" {
t.Error("hash do bloco não pode ser vazio")
}
if block.Index != 2 {
t.Errorf("esperava índice 2, obteve %d", block.Index)
}
if block.PrevHash != "prevhash" {
t.Errorf("hash anterior incorreto")
}
if len(block.Txns) != 2 {
t.Errorf("esperava 2 transações no bloco, obteve %d", len(block.Txns))
}
}

View File

@ -0,0 +1,74 @@
package transactions
import (
"errors"
"sync"
)
type Mempool struct {
txs []*Transaction
mu sync.RWMutex
}
func NewMempool() *Mempool {
return &Mempool{}
}
func (m *Mempool) Add(tx *Transaction) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Has(tx.Hash()) {
return errors.New("transação já existente na mempool")
}
m.txs = append(m.txs, tx)
return nil
}
func (m *Mempool) Remove(hash string) {
m.mu.Lock()
defer m.mu.Unlock()
for i, tx := range m.txs {
if tx.Hash() == hash {
m.txs = append(m.txs[:i], m.txs[i+1:]...)
return
}
}
}
func (m *Mempool) All() []*Transaction {
m.mu.RLock()
defer m.mu.RUnlock()
return append([]*Transaction(nil), m.txs...)
}
func (m *Mempool) GetByHash(hash string) *Transaction {
m.mu.RLock()
defer m.mu.RUnlock()
for _, tx := range m.txs {
if tx.Hash() == hash {
return tx
}
}
return nil
}
func (m *Mempool) Pending() []*Transaction {
return m.All()
}
func (m *Mempool) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
m.txs = []*Transaction{}
}
func (m *Mempool) Has(hash string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
for _, tx := range m.txs {
if tx.Hash() == hash {
return true
}
}
return false
}

View File

@ -0,0 +1,54 @@
package transactions_test
import (
"dejo_node/internal/transactions"
"testing"
)
func TestMempool_AddAndGet(t *testing.T) {
pool := transactions.NewMempool()
tx := &transactions.Transaction{
From: "0xabc",
To: "0xdef",
Nonce: 1,
Value: 10,
Gas: 1,
Signature: "sig",
}
err := pool.Add(tx)
if err != nil {
t.Fatalf("erro ao adicionar transação: %v", err)
}
txs := pool.All()
if len(txs) != 1 {
t.Errorf("esperava 1 transação, obteve %d", len(txs))
}
if !pool.Has(tx.Hash()) {
t.Error("transação deveria existir na mempool")
}
}
func TestMempool_Duplicates(t *testing.T) {
pool := transactions.NewMempool()
tx := &transactions.Transaction{From: "0xaaa", Nonce: 1}
_ = pool.Add(tx)
err := pool.Add(tx)
if err == nil {
t.Error("esperava erro de transação duplicada")
}
}
func TestMempool_Remove(t *testing.T) {
pool := transactions.NewMempool()
tx := &transactions.Transaction{From: "0xaaa", Nonce: 2}
_ = pool.Add(tx)
pool.Remove(tx.Hash())
if pool.Has(tx.Hash()) {
t.Error("transação ainda existe após remoção")
}
}

View File

@ -0,0 +1,33 @@
package transactions
import (
"sync"
)
// SeenTracker registra hashes de transações já vistas (para evitar replay).
type SeenTracker struct {
mu sync.RWMutex
hashes map[string]struct{}
}
// NewSeenTracker cria um novo tracker de transações vistas
func NewSeenTracker() *SeenTracker {
return &SeenTracker{
hashes: make(map[string]struct{}),
}
}
// Seen verifica se uma transação já foi vista
func (s *SeenTracker) Seen(hash string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, exists := s.hashes[hash]
return exists
}
// Mark marca uma transação como vista
func (s *SeenTracker) Mark(hash string) {
s.mu.Lock()
defer s.mu.Unlock()
s.hashes[hash] = struct{}{}
}

View File

@ -0,0 +1,29 @@
package transactions
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
type Transaction struct {
Type string // TRANSFER, STAKE, MINT, etc.
From string
To string
Value float64
Nonce uint64
Gas uint64
Signature string
}
// Hash gera um hash SHA256 da transação para identificação única
func (tx *Transaction) Hash() string {
data := fmt.Sprintf("%s:%s:%f:%d:%d", tx.From, tx.To, tx.Value, tx.Nonce, tx.Gas)
sum := sha256.Sum256([]byte(data))
return hex.EncodeToString(sum[:])
}
// IsZero valida se os campos obrigatórios estão presentes
func (tx *Transaction) IsZero() bool {
return tx.From == "" || tx.To == "" || tx.Value == 0
}

View File

@ -0,0 +1,39 @@
package transactions_test
import (
"dejo_node/internal/transactions"
"testing"
)
func TestTransaction_Hash(t *testing.T) {
tx := transactions.Transaction{
From: "0xabc",
To: "0xdef",
Value: 100,
Nonce: 1,
Gas: 21000,
Signature: "0xsig",
}
hash := tx.Hash()
if hash == "" {
t.Fatal("hash não pode ser vazio")
}
}
func TestTransaction_IsZero(t *testing.T) {
tx := transactions.Transaction{}
if !tx.IsZero() {
t.Error("transação vazia deveria retornar true para IsZero")
}
tx = transactions.Transaction{
From: "0xabc",
To: "0xdef",
Value: 10,
Signature: "0xsig",
}
if tx.IsZero() {
t.Error("transação válida retornou true para IsZero")
}
}

View File

@ -0,0 +1,63 @@
package transactions
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math/big"
)
// IsValid realiza validações na transação: assinatura, saldo e nonce.
func (tx *Transaction) IsValid(pubKey *ecdsa.PublicKey, balance float64, currentNonce uint64) error {
if tx.IsZero() {
return errors.New("transação malformada: campos obrigatórios ausentes")
}
if tx.Value+float64(tx.Gas) > balance {
return fmt.Errorf("saldo insuficiente: necessário %.2f, disponível %.2f", tx.Value+float64(tx.Gas), balance)
}
if tx.Nonce != currentNonce {
return fmt.Errorf("nonce inválido: esperado %d, recebido %d", currentNonce, tx.Nonce)
}
r, s, err := parseSignature(tx.Signature)
if err != nil {
return fmt.Errorf("erro ao parsear assinatura: %v", err)
}
hash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%f:%d:%d",
tx.From, tx.To, tx.Value, tx.Nonce, tx.Gas)))
if !ecdsa.Verify(pubKey, hash[:], r, s) {
return errors.New("assinatura inválida")
}
return nil
}
// parseSignature converte a assinatura hex (formato r||s) em *big.Int.
func parseSignature(sig string) (*big.Int, *big.Int, error) {
bytes, err := hex.DecodeString(sig)
if err != nil {
return nil, nil, err
}
if len(bytes) != 64 {
return nil, nil, errors.New("assinatura deve ter 64 bytes (r||s)")
}
r := new(big.Int).SetBytes(bytes[:32])
s := new(big.Int).SetBytes(bytes[32:])
return r, s, nil
}
// GenerateSignature é uma função auxiliar (para testes): assina tx com chave privada.
func (tx *Transaction) GenerateSignature(priv *ecdsa.PrivateKey) string {
hash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%f:%d:%d",
tx.From, tx.To, tx.Value, tx.Nonce, tx.Gas)))
r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:])
return fmt.Sprintf("%064x%064x", r, s)
}

View File

@ -0,0 +1,48 @@
package transactions_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"dejo_node/internal/transactions"
"testing"
)
func TestTransaction_IsValid(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pub := &priv.PublicKey
tx := transactions.Transaction{
From: "0xabc",
To: "0xdef",
Value: 50,
Gas: 10,
Nonce: 1,
}
tx.Signature = tx.GenerateSignature(priv)
err := tx.IsValid(pub, 100, 1)
if err != nil {
t.Errorf("esperava transação válida, mas falhou: %v", err)
}
}
func TestTransaction_IsValid_InvalidSignature(t *testing.T) {
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
wrongPriv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pub := &priv.PublicKey
tx := transactions.Transaction{
From: "0xabc",
To: "0xdef",
Value: 50,
Gas: 10,
Nonce: 1,
}
tx.Signature = tx.GenerateSignature(wrongPriv)
err := tx.IsValid(pub, 100, 1)
if err == nil {
t.Error("esperava falha na assinatura, mas foi aceita")
}
}

20
internal/ws/events.go Normal file
View File

@ -0,0 +1,20 @@
package ws
import (
"encoding/json"
"fmt"
)
// Emit envia um evento para todos os clientes conectados
func Emit(eventType string, data any) {
msg := map[string]any{
"type": eventType,
"data": data,
}
jsonMsg, err := json.Marshal(msg)
if err != nil {
fmt.Println("Erro ao serializar evento WebSocket:", err)
return
}
broadcast <- jsonMsg
}

44
internal/ws/hub.go Normal file
View File

@ -0,0 +1,44 @@
package ws
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// Hub representa um cliente WebSocket
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan []byte)
// StartHub inicia o hub de websocket
func StartHub() {
go func() {
for {
msg := <-broadcast
for client := range clients {
err := client.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Printf("WebSocket erro: %v", err)
client.Close()
delete(clients, client)
}
}
}
}()
}
// HandleWS aceita conexões websocket
func HandleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Erro ao fazer upgrade para websocket: %v", err)
return
}
clients[conn] = true
log.Println("Novo cliente WebSocket conectado")
}