commit inicial do projeto
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user