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