commit inicial do projeto
This commit is contained in:
82
internal/api/account_handlers.go
Normal file
82
internal/api/account_handlers.go
Normal 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)
|
||||
}
|
||||
31
internal/api/block_handlers.go
Normal file
31
internal/api/block_handlers.go
Normal 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
15
internal/api/dao_api.go
Normal 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)
|
||||
84
internal/api/dao_handlers.go
Normal file
84
internal/api/dao_handlers.go
Normal 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
20
internal/api/handler.go
Normal 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
10
internal/api/health.go
Normal 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"))
|
||||
}
|
||||
11
internal/api/heartbeat_handler.go
Normal file
11
internal/api/heartbeat_handler.go
Normal 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
15
internal/api/liveness.go
Normal 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"))
|
||||
}
|
||||
91
internal/api/middleware.go
Normal file
91
internal/api/middleware.go
Normal 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
42
internal/api/router.go
Normal 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
21
internal/api/server.go
Normal 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
|
||||
}
|
||||
53
internal/api/staking_handlers.go
Normal file
53
internal/api/staking_handlers.go
Normal 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)
|
||||
}
|
||||
30
internal/api/tx_handlers.go
Normal file
30
internal/api/tx_handlers.go
Normal 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
22
internal/api/utils.go
Normal 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,
|
||||
})
|
||||
}
|
||||
13
internal/blockchain/block.go
Normal file
13
internal/blockchain/block.go
Normal 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
27
internal/config/config.go
Normal 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{}
|
||||
}
|
||||
15
internal/consensus/consensus.go
Normal file
15
internal/consensus/consensus.go
Normal 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
|
||||
}
|
||||
15
internal/consensus/engine.go
Normal file
15
internal/consensus/engine.go
Normal 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
|
||||
}
|
||||
72
internal/consensus/finality.go
Normal file
72
internal/consensus/finality.go
Normal 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)
|
||||
}
|
||||
78
internal/consensus/liveness.go
Normal file
78
internal/consensus/liveness.go
Normal 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]
|
||||
}
|
||||
54
internal/consensus/messages.go
Normal file
54
internal/consensus/messages.go
Normal 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
|
||||
}
|
||||
39
internal/consensus/network.go
Normal file
39
internal/consensus/network.go
Normal 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
53
internal/consensus/pos.go
Normal 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
|
||||
}
|
||||
18
internal/consensus/proposer.go
Normal file
18
internal/consensus/proposer.go
Normal 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
|
||||
}
|
||||
55
internal/consensus/quorum.go
Normal file
55
internal/consensus/quorum.go
Normal 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, ""
|
||||
}
|
||||
174
internal/consensus/round_loop.go
Normal file
174
internal/consensus/round_loop.go
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
23
internal/consensus/simple/finality.go
Normal file
23
internal/consensus/simple/finality.go
Normal 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)
|
||||
}
|
||||
69
internal/consensus/simple/simple.go
Normal file
69
internal/consensus/simple/simple.go
Normal 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"
|
||||
}
|
||||
28
internal/consensus/slash.go
Normal file
28
internal/consensus/slash.go
Normal 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")
|
||||
}
|
||||
42
internal/consensus/state.go
Normal file
42
internal/consensus/state.go
Normal 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()
|
||||
}
|
||||
109
internal/consensus/transport_http.go
Normal file
109
internal/consensus/transport_http.go
Normal 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
|
||||
}
|
||||
64
internal/consensus/validator_set.go
Normal file
64
internal/consensus/validator_set.go
Normal 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
21
internal/crypto/crypto.go
Normal 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
7
internal/crypto/keys.go
Normal 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
|
||||
26
internal/crypto/pq_signer.go
Normal file
26
internal/crypto/pq_signer.go
Normal 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
27
internal/crypto/signer.go
Normal 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
104
internal/dao/proposal.go
Normal 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
78
internal/dao/store.go
Normal 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)
|
||||
}
|
||||
25
internal/mempool/mempool.go
Normal file
25
internal/mempool/mempool.go
Normal 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
76
internal/miner/miner.go
Normal 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
|
||||
}
|
||||
57
internal/monitor/heartbeat.go
Normal file
57
internal/monitor/heartbeat.go
Normal 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]
|
||||
}
|
||||
43
internal/monitor/monitor.go
Normal file
43
internal/monitor/monitor.go
Normal 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]
|
||||
}
|
||||
61
internal/monitoring/metrics.go
Normal file
61
internal/monitoring/metrics.go
Normal 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()
|
||||
}
|
||||
59
internal/oracle/fetcher.go
Normal file
59
internal/oracle/fetcher.go
Normal 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
|
||||
}
|
||||
20
internal/oracle/fetcher_test.go
Normal file
20
internal/oracle/fetcher_test.go
Normal 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
49
internal/oracle/store.go
Normal 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]
|
||||
}
|
||||
30
internal/oracle/store_test.go
Normal file
30
internal/oracle/store_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
45
internal/oracle/validator.go
Normal file
45
internal/oracle/validator.go
Normal 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")
|
||||
}
|
||||
28
internal/oracle/validator_test.go
Normal file
28
internal/oracle/validator_test.go
Normal 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
52
internal/p2p/dht.go
Normal 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
66
internal/p2p/host.go
Normal 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
58
internal/p2p/limiter.go
Normal 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
16
internal/p2p/network.go
Normal 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
81
internal/p2p/p2p.go
Normal 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
35
internal/p2p/p2p_test.go
Normal 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)
|
||||
}
|
||||
19
internal/staking/rewards.go
Normal file
19
internal/staking/rewards.go
Normal 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
82
internal/staking/store.go
Normal 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
8
internal/state/apply.go
Normal 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
7
internal/state/domain.go
Normal 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
99
internal/state/state.go
Normal 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)
|
||||
}
|
||||
24
internal/storage/snapshot.go
Normal file
24
internal/storage/snapshot.go
Normal 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)
|
||||
}
|
||||
8
internal/storage/storage.go
Normal file
8
internal/storage/storage.go
Normal 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
86
internal/storage/store.go
Normal 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
68
internal/sync/height.go
Normal 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
33
internal/sync/sync.go
Normal 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
|
||||
}
|
||||
48
internal/transactions/block.go
Normal file
48
internal/transactions/block.go
Normal 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()
|
||||
}
|
||||
41
internal/transactions/block_test.go
Normal file
41
internal/transactions/block_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
74
internal/transactions/mempool.go
Normal file
74
internal/transactions/mempool.go
Normal 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
|
||||
}
|
||||
54
internal/transactions/mempool_test.go
Normal file
54
internal/transactions/mempool_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
33
internal/transactions/replay.go
Normal file
33
internal/transactions/replay.go
Normal 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{}{}
|
||||
}
|
||||
29
internal/transactions/transaction.go
Normal file
29
internal/transactions/transaction.go
Normal 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
|
||||
}
|
||||
39
internal/transactions/transaction_test.go
Normal file
39
internal/transactions/transaction_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
63
internal/transactions/validation.go
Normal file
63
internal/transactions/validation.go
Normal 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)
|
||||
}
|
||||
48
internal/transactions/validation_test.go
Normal file
48
internal/transactions/validation_test.go
Normal 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
20
internal/ws/events.go
Normal 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
44
internal/ws/hub.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user