From 8f04473c0b1766e554d41318a3c26b189c864133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAnior?= Date: Fri, 23 May 2025 10:44:32 -0300 Subject: [PATCH] commit inicial do projeto --- Makefile | 23 + README.md | 71 +++ build/Dockerfile | 22 + changelog.md | 50 ++ changelog.txt | 23 + cmd/main.go | 73 +++ config/config.yaml | 10 + data/blocks/block_001.json | 8 + data/blocks/block_002.json | 8 + data/blocks/block_003.json | 8 + data/blocks/block_004.json | 8 + data/staking.db | Bin 0 -> 89 bytes data/state.gob | Bin 0 -> 36 bytes docs/api.md | 95 ++++ docs/architecture.md | 37 ++ docs/consensus.md | 61 +++ docs/healthchecks.md | 108 ++++ docs/kubernetes.md | 131 +++++ docs/logging.md | 91 ++++ docs/openapi.yaml | 90 ++++ docs/planning.md | 92 ++++ docs/roadmap_v1.0.0.md | 182 +++++++ docs/tasks_todo.md | 56 ++ go.mod | 139 +++++ go.sum | 628 ++++++++++++++++++++++ internal/api/account_handlers.go | 82 +++ internal/api/block_handlers.go | 31 ++ internal/api/dao_api.go | 15 + internal/api/dao_handlers.go | 84 +++ internal/api/handler.go | 20 + internal/api/health.go | 10 + internal/api/heartbeat_handler.go | 11 + internal/api/liveness.go | 15 + internal/api/middleware.go | 91 ++++ internal/api/router.go | 42 ++ internal/api/server.go | 21 + internal/api/staking_handlers.go | 53 ++ internal/api/tx_handlers.go | 30 ++ internal/api/utils.go | 22 + internal/blockchain/block.go | 13 + internal/config/config.go | 27 + internal/consensus/consensus.go | 15 + internal/consensus/engine.go | 15 + internal/consensus/finality.go | 72 +++ internal/consensus/liveness.go | 78 +++ internal/consensus/messages.go | 54 ++ internal/consensus/network.go | 39 ++ internal/consensus/pos.go | 53 ++ internal/consensus/proposer.go | 18 + internal/consensus/quorum.go | 55 ++ internal/consensus/round_loop.go | 174 ++++++ internal/consensus/simple/finality.go | 23 + internal/consensus/simple/simple.go | 69 +++ internal/consensus/slash.go | 28 + internal/consensus/state.go | 42 ++ internal/consensus/transport_http.go | 109 ++++ internal/consensus/validator_set.go | 64 +++ internal/crypto/crypto.go | 21 + internal/crypto/keys.go | 7 + internal/crypto/pq_signer.go | 26 + internal/crypto/signer.go | 27 + internal/dao/proposal.go | 104 ++++ internal/dao/store.go | 78 +++ internal/mempool/mempool.go | 25 + internal/miner/miner.go | 76 +++ internal/monitor/heartbeat.go | 57 ++ internal/monitor/monitor.go | 43 ++ internal/monitoring/metrics.go | 61 +++ internal/oracle/fetcher.go | 59 ++ internal/oracle/fetcher_test.go | 20 + internal/oracle/store.go | 49 ++ internal/oracle/store_test.go | 30 ++ internal/oracle/validator.go | 45 ++ internal/oracle/validator_test.go | 28 + internal/p2p/dht.go | 52 ++ internal/p2p/host.go | 66 +++ internal/p2p/limiter.go | 58 ++ internal/p2p/network.go | 16 + internal/p2p/p2p.go | 81 +++ internal/p2p/p2p_test.go | 35 ++ internal/staking/rewards.go | 19 + internal/staking/store.go | 82 +++ internal/state/apply.go | 8 + internal/state/domain.go | 7 + internal/state/state.go | 99 ++++ internal/storage/snapshot.go | 24 + internal/storage/storage.go | 8 + internal/storage/store.go | 86 +++ internal/sync/height.go | 68 +++ internal/sync/sync.go | 33 ++ internal/transactions/block.go | 48 ++ internal/transactions/block_test.go | 41 ++ internal/transactions/mempool.go | 74 +++ internal/transactions/mempool_test.go | 54 ++ internal/transactions/replay.go | 33 ++ internal/transactions/transaction.go | 29 + internal/transactions/transaction_test.go | 39 ++ internal/transactions/validation.go | 63 +++ internal/transactions/validation_test.go | 48 ++ internal/ws/events.go | 20 + internal/ws/hub.go | 44 ++ main | Bin 0 -> 12452162 bytes project copy.md | 80 +++ project.txt | 95 ++++ scripts/start-node-A.sh | 9 + scripts/start-node-B.sh | 9 + 106 files changed, 5673 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 build/Dockerfile create mode 100644 changelog.md create mode 100644 changelog.txt create mode 100644 cmd/main.go create mode 100644 config/config.yaml create mode 100644 data/blocks/block_001.json create mode 100644 data/blocks/block_002.json create mode 100644 data/blocks/block_003.json create mode 100644 data/blocks/block_004.json create mode 100644 data/staking.db create mode 100644 data/state.gob create mode 100644 docs/api.md create mode 100644 docs/architecture.md create mode 100644 docs/consensus.md create mode 100644 docs/healthchecks.md create mode 100644 docs/kubernetes.md create mode 100644 docs/logging.md create mode 100644 docs/openapi.yaml create mode 100644 docs/planning.md create mode 100644 docs/roadmap_v1.0.0.md create mode 100644 docs/tasks_todo.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/account_handlers.go create mode 100644 internal/api/block_handlers.go create mode 100644 internal/api/dao_api.go create mode 100644 internal/api/dao_handlers.go create mode 100644 internal/api/handler.go create mode 100644 internal/api/health.go create mode 100644 internal/api/heartbeat_handler.go create mode 100644 internal/api/liveness.go create mode 100644 internal/api/middleware.go create mode 100644 internal/api/router.go create mode 100644 internal/api/server.go create mode 100644 internal/api/staking_handlers.go create mode 100644 internal/api/tx_handlers.go create mode 100644 internal/api/utils.go create mode 100644 internal/blockchain/block.go create mode 100644 internal/config/config.go create mode 100644 internal/consensus/consensus.go create mode 100644 internal/consensus/engine.go create mode 100644 internal/consensus/finality.go create mode 100644 internal/consensus/liveness.go create mode 100644 internal/consensus/messages.go create mode 100644 internal/consensus/network.go create mode 100644 internal/consensus/pos.go create mode 100644 internal/consensus/proposer.go create mode 100644 internal/consensus/quorum.go create mode 100644 internal/consensus/round_loop.go create mode 100644 internal/consensus/simple/finality.go create mode 100644 internal/consensus/simple/simple.go create mode 100644 internal/consensus/slash.go create mode 100644 internal/consensus/state.go create mode 100644 internal/consensus/transport_http.go create mode 100644 internal/consensus/validator_set.go create mode 100644 internal/crypto/crypto.go create mode 100644 internal/crypto/keys.go create mode 100644 internal/crypto/pq_signer.go create mode 100644 internal/crypto/signer.go create mode 100644 internal/dao/proposal.go create mode 100644 internal/dao/store.go create mode 100644 internal/mempool/mempool.go create mode 100644 internal/miner/miner.go create mode 100644 internal/monitor/heartbeat.go create mode 100644 internal/monitor/monitor.go create mode 100644 internal/monitoring/metrics.go create mode 100644 internal/oracle/fetcher.go create mode 100644 internal/oracle/fetcher_test.go create mode 100644 internal/oracle/store.go create mode 100644 internal/oracle/store_test.go create mode 100644 internal/oracle/validator.go create mode 100644 internal/oracle/validator_test.go create mode 100644 internal/p2p/dht.go create mode 100644 internal/p2p/host.go create mode 100644 internal/p2p/limiter.go create mode 100644 internal/p2p/network.go create mode 100644 internal/p2p/p2p.go create mode 100644 internal/p2p/p2p_test.go create mode 100644 internal/staking/rewards.go create mode 100644 internal/staking/store.go create mode 100644 internal/state/apply.go create mode 100644 internal/state/domain.go create mode 100644 internal/state/state.go create mode 100644 internal/storage/snapshot.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/store.go create mode 100644 internal/sync/height.go create mode 100644 internal/sync/sync.go create mode 100644 internal/transactions/block.go create mode 100644 internal/transactions/block_test.go create mode 100644 internal/transactions/mempool.go create mode 100644 internal/transactions/mempool_test.go create mode 100644 internal/transactions/replay.go create mode 100644 internal/transactions/transaction.go create mode 100644 internal/transactions/transaction_test.go create mode 100644 internal/transactions/validation.go create mode 100644 internal/transactions/validation_test.go create mode 100644 internal/ws/events.go create mode 100644 internal/ws/hub.go create mode 100755 main create mode 100644 project copy.md create mode 100644 project.txt create mode 100755 scripts/start-node-A.sh create mode 100755 scripts/start-node-B.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..53cad9e --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +# Makefile para gerenciamento do DEJO Node + +.PHONY: build run test docker clean + +build: + @echo "🔧 Compilando o DEJO Node..." + go build -o dejo-node ./cmd/main.go + +run: build + @echo "🚀 Executando o DEJO Node..." + ./dejo-node + +test: + @echo "🧪 Executando testes..." + go test ./... + +docker: + @echo "🐳 Criando imagem Docker..." + docker build -t dejo-node:latest ./build/ + +clean: + @echo "🧹 Limpando arquivos temporários..." + rm -f dejo-node \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b84665c --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# 🏗️ DEJO Node - Core da Blockchain + +## 📌 Visão Geral + +O **DEJO Node** é o núcleo da **blockchain DEJO**, responsável por **validação de transações, consenso, geração de blocos e armazenamento descentralizado**. Ele garante **segurança, escalabilidade e integridade** dos dados na rede. + +Para detalhes técnicos e implementação, veja a **documentação completa**: + +🔹 **[Arquitetura do Sistema](docs/architecture.md)** +🔹 **[Mecanismo de Consenso](docs/consensus.md)** +🔹 **[API RPC/WebSockets](docs/api.md)** +🔹 **[Healthchecks](docs/healthchecks.md)** +🔹 **[Logging e Auditoria](docs/logging.md)** +🔹 **[Deploy no Kubernetes](docs/kubernetes.md)** +🔹 **[Planejamento de Desenvolvimento](planning.md)** + +--- + +## 🎯 Funcionalidades Principais + +✅ **Validação de transações** com criptografia de chave pública. +✅ **Criação e sincronização de blocos** entre os nós. +✅ **Implementação do consenso PoS/BFT** para garantir segurança. +✅ **Persistência de dados com LevelDB/BadgerDB** para escalabilidade. +✅ **API RPC/WebSockets** para interação com a rede. +✅ **Proteção contra ataques Sybil, DDoS e replay attacks**. +✅ **Mecanismo de auditoria on-chain para monitoramento da rede**. +✅ **Criptografia pós-quântica para garantir segurança futura**. + +ℹ️ **Staking e Governança** são gerenciados no microserviço [`dejo-governance`](../dejo-governance/README.md). + +--- + +## 🚀 Como Rodar o DEJO Node + +```sh +# Clonar o repositório +git clone https://github.com/dejo/dejo_node.git +cd dejo_node + +# Instalar dependências +go mod tidy + +# Rodar um nó da blockchain +go run cmd/main.go +``` + +--- + +## 🔬 Testes e Validação + +Os testes cobrem validação de blocos, execução de transações e sincronização P2P. + +```sh +# Rodar testes unitários +go test ./... +``` + +**📌 Testes adicionais planejados:** +- Testes de stress para verificar escalabilidade. +- Simulação de ataques para validar segurança. +- Recuperação após falha para garantir robustez. + +--- + +## 📖 Documentação e Referências + +- [Whitepaper Técnico da DEJO](../Whitepaper_Tecnico.pdf) +- [Plano de Negócios - DEJO Digital Assets](../Plano_de_Negocios.pdf) + +🚀 **DEJO Node: O Coração da Blockchain!** 🔥 \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..d6330f0 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,22 @@ +# Dockerfile para o DEJO Node + +FROM golang:1.20 as builder +WORKDIR /app + +# Copiar arquivos e instalar dependências +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN go build -o dejo-node ./cmd/main.go + +# Criar imagem final +FROM debian:bullseye-slim +WORKDIR /root/ + +COPY --from=builder /app/dejo-node ./dejo-node + +# Definir variáveis de ambiente padrão +ENV CONFIG_PATH="/config/config.yaml" + +CMD ["./dejo-node"] \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..fbbb010 --- /dev/null +++ b/changelog.md @@ -0,0 +1,50 @@ +# 📦 Changelog - DEJO Node + +## ✅ Versão 1.0.0 (Finalizada) + +### 🏗️ Estrutura Inicial +- Organização de diretórios `cmd/`, `internal/`, `pkg/` +- Dockerfile, Makefile e configs prontos +- `main.go` funcional + +### 🔐 Transações +- Estrutura básica de TX (`from`, `to`, `value`, `nonce`, `signature`) +- Validações e verificação de assinatura +- Mempool integrada + +### 📦 Armazenamento +- Implementação com LevelDB +- Indexação por hash e blocos + +### 🌐 Comunicação P2P +- libp2p com detecção automática +- Proteção Sybil (limite de conexões, delay por PeerID) +- Logs de conexões e desconexões + +### ⚖️ Consenso +- Algoritmo mínimo viável tipo PoA/BFT +- Validação e finalização de blocos + +### 🔗 API REST +- Endpoints: `/tx`, `/tx/{hash}`, `/block/{hash}`, `/mempool` +- Health checks: `/health`, `/startup`, `/ready` + +### 🗳️ Governança +- Estrutura de staking e votação +- RPCs para propostas e eleição + +### 🛰️ Oráculos +- Placeholder e endpoint para feed externo validado + +### 🔐 Segurança +- Rate Limiting anti-DDoS +- Monitoramento de conexões suspeitas +- Modularização para reputação de peer + +### 📊 DevOps +- `/metrics` com Prometheus (`tx_total`, `uptime`) +- `openapi.yaml` com documentação completa + +--- + +🚀 Projeto DEJO Node pronto para produção modular! diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..902bc3b --- /dev/null +++ b/changelog.txt @@ -0,0 +1,23 @@ +# 📜 DEJO Node — Changelog Técnico + +## [2025-04-27] 🚀 Multi-node DEJO Chain com Consenso Distribuído via HTTP + +### ✅ Funcionalidades entregues: + +#### 🔥 Comunicação real entre nós +- Implementação de transporte HTTP (`HTTPTransport`) para Broadcast de mensagens de consenso (Proposal, Prevote, Precommit) +- Cada nó expõe `/consensus` para receber mensagens +- Lista de peers (`DEJO_PEERS`) via variável de ambiente + +#### 🔁 Multi-node funcional +- Auto-stake automático para todos os nós se necessário +- Consenso BFT ponderado rodando entre múltiplos validadores +- Blocos finalizados em conjunto e persistidos + +#### 🧰 Scripts de inicialização +- `scripts/start-node-A.sh` +- `scripts/start-node-B.sh` +- Facilita testes locais e simulação de rede + +--- +Próximo desafio sugerido: Resiliência e Liveness Detection entre os peers 🚀 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..8b08672 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "dejo_node/internal/api" + "dejo_node/internal/consensus" + "dejo_node/internal/mempool" + "dejo_node/internal/monitor" + "dejo_node/internal/staking" + "dejo_node/internal/state" + "dejo_node/internal/storage" + "dejo_node/internal/transactions" + "fmt" + "net/http" + "os" + "strings" + "time" +) + +func main() { + ctx := context.Background() + + nodeID := os.Getenv("NODE_ID") + port := os.Getenv("DEJO_PORT") + validators := strings.Split(os.Getenv("DEJO_VALIDATORS"), ",") + peers := strings.Split(os.Getenv("DEJO_PEERS"), ",") + + blockStore := storage.NewBlockStore() + stakingStore := staking.NewStakingStore() + mp := mempool.NewMempool() + globalState := state.NewState() + + // Heartbeat function + pingPeer := func(peer string) bool { + client := http.Client{Timeout: 2 * time.Second} + resp, err := client.Get(peer + "/health") + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK + } + + monitor := monitor.NewMonitor(pingPeer) + + handler := api.NewHandler(blockStore, stakingStore, mp, globalState) + + server := api.NewServer(handler) + go func() { + fmt.Printf("🌐 Servidor HTTP iniciado na porta :%s\n", port) + http.ListenAndServe(":"+port, server) + }() + + round := consensus.NewRoundState(1) + transport := consensus.NewHTTPTransport() + + consensus.StartConsensusLoop( + ctx, + nodeID, + round, + transport.Receive, + 700, + blockStore, + func() *transactions.Block { + return &transactions.Block{ + Txns: mp.GetTransactions(), + } + }, + stakingStore, + globalState, + monitor, + ) +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..6e678bf --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,10 @@ +# Configuração do DEJO Node + +environment: production +log_level: info +consensus_algorithm: PoS +rpc_port: 8545 +ws_port: 8546 +database: + type: leveldb + path: "/data/blockchain" \ No newline at end of file diff --git a/data/blocks/block_001.json b/data/blocks/block_001.json new file mode 100644 index 0000000..a160d6a --- /dev/null +++ b/data/blocks/block_001.json @@ -0,0 +1,8 @@ +{ + "Index": 1, + "PrevHash": "genesis", + "Txns": [], + "Timestamp": 1746203588, + "Nonce": 0, + "Hash": "176b199bd82d6e6d22719cdc95709646433204f299b2f49efbba9985ee9eac4e" +} \ No newline at end of file diff --git a/data/blocks/block_002.json b/data/blocks/block_002.json new file mode 100644 index 0000000..808a91f --- /dev/null +++ b/data/blocks/block_002.json @@ -0,0 +1,8 @@ +{ + "Index": 2, + "PrevHash": "176b199bd82d6e6d22719cdc95709646433204f299b2f49efbba9985ee9eac4e", + "Txns": [], + "Timestamp": 1746203596, + "Nonce": 0, + "Hash": "0af7272a293becc055d79604b96717e06be4e3ff8be65f1d65b6d1979b9d166c" +} \ No newline at end of file diff --git a/data/blocks/block_003.json b/data/blocks/block_003.json new file mode 100644 index 0000000..aa99198 --- /dev/null +++ b/data/blocks/block_003.json @@ -0,0 +1,8 @@ +{ + "Index": 3, + "PrevHash": "0af7272a293becc055d79604b96717e06be4e3ff8be65f1d65b6d1979b9d166c", + "Txns": [], + "Timestamp": 1746206836, + "Nonce": 0, + "Hash": "40186b92c0be8c1cd252b223dce51df3156a3fadd32e21f38db298e01da70f69" +} \ No newline at end of file diff --git a/data/blocks/block_004.json b/data/blocks/block_004.json new file mode 100644 index 0000000..a2a3086 --- /dev/null +++ b/data/blocks/block_004.json @@ -0,0 +1,8 @@ +{ + "Index": 4, + "PrevHash": "40186b92c0be8c1cd252b223dce51df3156a3fadd32e21f38db298e01da70f69", + "Txns": [], + "Timestamp": 1746206844, + "Nonce": 0, + "Hash": "4519522e80850a467c571eccdbad3a9b078b75d0dd3c889a87725c207eff3464" +} \ No newline at end of file diff --git a/data/staking.db b/data/staking.db new file mode 100644 index 0000000000000000000000000000000000000000..2ec7ad1587501c2397ecf83c0b09d69fe04e8666 GIT binary patch literal 89 zcmd=8-^jwq^uLLLk%#er0|SF{Ju{Hoz`)4F$mW=vUz%6K$i~3P;Zj Implementar estrutura de dados que armazena contas, com saldo e nonce. + +- Criar uma struct `Account`: + ```go + type Account struct { + Balance uint64 + Nonce uint64 + } + ``` +- Criar um `map[string]*Account` que representa o conjunto de contas (`state.accounts`) +- O acesso será via endereço (`string`) como chave +- Contas devem ser inicializadas com `Balance = 0` caso ainda não existam + +--- + +#### **1.2. Aplicar Transações** +> Criar lógica que modifica o estado de contas ao aplicar uma transação do tipo `Transfer`. + +- Criar função: + ```go + func (s *State) ApplyTransaction(tx *Transaction) error + ``` +- Verificar se: + - Conta `from` existe + - `from.Balance >= tx.Value` + - `tx.Nonce == from.Nonce` +- Aplicar mudanças: + - Deduzir `Value` da conta `from` + - Acrescentar `Value` à conta `to` + - Incrementar `Nonce` da conta `from` +- Retornar erro se transação for inválida + +--- + +#### **1.3. Executar lote de transações ao final do bloco** +> Aplicar todas as transações do bloco no commit. + +- No final do `StartConsensusLoop`, quando o bloco é finalizado: + ```go + for _, tx := range block.Txns { + err := state.ApplyTransaction(tx) + if err != nil { + log.Printf("❌ Erro ao aplicar tx: %+v\n", err) + } + } + ``` + +--- + +### 📚 2. Módulo de Estado (`internal/state`) + +#### **2.1. Estrutura do módulo `state`** +> Centralizar toda lógica de mutação de estado (contas, staking, dao). + +- Criar `internal/state/state.go` +- Definir `State`: + ```go + type State struct { + Accounts map[string]*Account + Staking *staking.StakingStore + DAO *dao.DAOStore + } + ``` +- Implementar métodos: + - `ApplyTransaction(tx *Transaction) error` + - `GetBalance(address string) uint64` + - `Mint(address string, value uint64)` + - `SaveToDisk(path string)` / `LoadFromDisk(path string)` + +--- + +#### **2.2. Encapsular staking/DAO dentro de State** +> Injetar dependência de staking e DAO no state para simplificar o acesso global. + +- Criar `NewState()` que instancia os campos `Accounts`, `Staking` e `DAO` +- Atualizar consenso e handlers para usar `state.Staking` e `state.DAO` em vez de referências diretas + +--- + +### 💸 3. Recompensas e Penalidades + +#### **3.1. Recompensa** +> Premiar validadores que participaram corretamente da rodada. + +- No commit do bloco: + - Calcular stake total que participou + - Distribuir `totalReward = 5 tokens` proporcionalmente: + ```go + reward := (validatorStake * totalReward) / totalStake + state.Mint(validatorID, reward) + ``` +- Validadores que não participaram não recebem nada + +--- + +#### **3.2. Slash** +> Penalizar validadores que não participaram da rodada. + +- Após o commit: + - Identificar validadores ativos que **não** fizeram `PRECOMMIT` + - Deduzir 50 tokens via: + ```go + state.accounts[validator].Balance -= 50 + ``` + +--- + +### 🌐 4. API REST Estendida + +#### **4.1. `/accounts/{addr}`** +> Obter saldo e informações de conta. + +- GET `/accounts/{addr}` +- Retornar: + ```json + { + "address": "node-A", + "balance": 950, + "nonce": 3 + } + ``` + +--- + +#### **4.2. `/validators`** +> Obter todos validadores ativos. + +- GET `/validators` +- Retornar lista: + ```json + [ + { "address": "node-A", "stake": 1000, "status": "online" }, + ... + ] + ``` + +--- + +#### **4.3. `/dao/status`** +> Visualizar estado atual da DAO. + +- GET `/dao/status` +- Retornar: + - Propostas em aberto + - Número de votos + - % de quorum atingido + - Tempo restante + +--- + +### 🧪 5. Testes + +#### **5.1. Testes com múltiplos nós** +> Verificar se transações em node-A aparecem em blocos de node-B. + +- Lançar `node-A` e `node-B` +- Enviar `POST /transaction` em A +- Verificar `GET /blocks` em B e encontrar a transação + +--- + +#### **5.2. Validação de saldo** +> Testar aplicação de transações. + +- Verificar que saldo de `from` diminui +- Verificar que saldo de `to` aumenta +- Verificar que `Nonce` incrementa + +--- + +#### **5.3. Testes de recompensas e penalidades** +> Validar que rewards/slash funcionam corretamente. + +- Propor bloco com `node-A`, verificar +5 tokens +- Omissão de `PRECOMMIT` de `node-B`, verificar -50 tokens \ No newline at end of file diff --git a/docs/tasks_todo.md b/docs/tasks_todo.md new file mode 100644 index 0000000..d270672 --- /dev/null +++ b/docs/tasks_todo.md @@ -0,0 +1,56 @@ +# ✅ Checklist Detalhado de Tarefas Pendentes - DEJO Node + +Este arquivo será utilizado para acompanhar o progresso detalhado das pendências restantes até a finalização da primeira versão completa do `dejo_node`. + +--- + +## 🧪 Revisão e Correção de Funcionalidades Existentes + +- [ ] Verificar se todos os pacotes possuem `go build` e `go test` funcionando +- [ ] Consolidar estrutura de `Block` em `blockchain/block.go` +- [ ] Validar `finality.go` (consensus/simple) + - [ ] Verificar uso correto de `storage.Storage` + - [ ] Confirmar estrutura correta do bloco (sem campos desconhecidos) + +--- + +## 🔧 Healthcheck + +- [ ] Criar pacote `internal/healthcheck` + - [ ] Implementar endpoint `/live` + - [ ] Implementar endpoint `/ready` + - [ ] Implementar endpoint `/startup` + - [ ] Incluir integração com `main.go` + +--- + +## 🧠 Mempool + +- [ ] Validar se `mempool` realmente existe ou precisa ser recriado +- [ ] Se necessário, recriar: + - [ ] Estrutura de pool de transações pendentes + - [ ] Métodos para adicionar, validar e expurgar transações + +--- + +## 🔌 RPC/WebSockets + +- [ ] Criar pacote `internal/rpc` + - [ ] Endpoint para consulta de bloco por hash + - [ ] Endpoint para envio de nova transação + - [ ] Suporte WebSocket para eventos em tempo real + +--- + +## 🔐 Security + +- [ ] Definir escopo inicial de `security` + - [ ] Criar proteção básica contra flood (ex.: IP rate limit) + - [ ] Adicionar logs de requisições suspeitas + +--- + +## 📄 Documentação e Planejamento + +- [ ] Atualizar `docs/planning.md` com estado real das fases +- [ ] Atualizar `/docs/tasks_todo.md` a cada entrega diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f5f976a --- /dev/null +++ b/go.mod @@ -0,0 +1,139 @@ +module dejo_node + +go 1.23.0 + +toolchain go1.23.2 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + github.com/libp2p/go-libp2p v0.41.1 + github.com/libp2p/go-libp2p-kad-dht v0.30.2 + github.com/multiformats/go-multiaddr v0.15.0 + github.com/prometheus/client_golang v1.21.1 + github.com/stretchr/testify v1.10.0 + github.com/syndtr/goleveldb v1.0.0 + github.com/ulule/limiter/v3 v3.11.2 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.3 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/boxo v0.28.0 // indirect + github.com/ipfs/go-cid v0.5.0 // indirect + github.com/ipfs/go-datastore v0.8.2 // indirect + github.com/ipfs/go-log v1.0.5 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/koron/go-ssdp v0.0.5 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-cidranger v1.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.5 // indirect + github.com/libp2p/go-libp2p-record v0.3.1 // indirect + github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.63 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/dtls/v3 v3.0.4 // indirect + github.com/pion/ice/v4 v4.0.8 // indirect + github.com/pion/interceptor v0.1.37 // indirect + github.com/pion/logging v0.2.3 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.11 // indirect + github.com/pion/sctp v1.8.37 // indirect + github.com/pion/sdp/v3 v3.0.10 // indirect + github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/webrtc/v4 v4.0.10 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polydawn/refmt v0.89.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.50.1 // indirect + github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/fx v1.23.0 // indirect + go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gonum.org/v1/gonum v0.15.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ac83700 --- /dev/null +++ b/go.sum @@ -0,0 +1,628 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= +github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= +github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/boxo v0.28.0 h1:io6nXqN8XMOstB7dQGG5GWnMk4WssoMvva9OADErZdI= +github.com/ipfs/boxo v0.28.0/go.mod h1:eY9w3iTpmZGKzDfEYjm3oK8f+xjv8KJhhNXJwicmd3I= +github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= +github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U= +github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= +github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-test v0.2.1 h1:/D/a8xZ2JzkYqcVcV/7HYlCnc7bv/pKHQiX5TdClkPE= +github.com/ipfs/go-test v0.2.1/go.mod h1:dzu+KB9cmWjuJnXFDYJwC25T3j1GcN57byN+ixmK39M= +github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= +github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= +github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= +github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= +github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.30.2 h1:K0LJPdXynQ+u3rx6uFlrfNy0i11LE6SOCDzwAAaahys= +github.com/libp2p/go-libp2p-kad-dht v0.30.2/go.mod h1:UV0mxF4ufh/ht05jNg5mcjOMrjK82uecgANa+GKi4y0= +github.com/libp2p/go-libp2p-kbucket v0.6.5 h1:Fsl1YvZcMwqrR4DYrTO02yo9PGYs2HBQIT3lGXFMTxg= +github.com/libp2p/go-libp2p-kbucket v0.6.5/go.mod h1:U6WOd0BvnSp03IQSrjgM54tg7zh1UUNsXLJqAQzClTA= +github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= +github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI= +github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= +github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= +github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo= +github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= +github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= +github.com/pion/ice/v4 v4.0.8 h1:ajNx0idNG+S+v9Phu4LSn2cs8JEfTsA1/tEjkkAVpFY= +github.com/pion/ice/v4 v4.0.8/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= +github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= +github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= +github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= +github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= +github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= +github.com/pion/webrtc/v4 v4.0.10 h1:Hq/JLjhqLxi+NmCtE8lnRPDr8H4LcNvwg8OxVcdv56Q= +github.com/pion/webrtc/v4 v4.0.10/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= +github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= +github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= +github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= +github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= +github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= +lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/api/account_handlers.go b/internal/api/account_handlers.go new file mode 100644 index 0000000..b1cbb86 --- /dev/null +++ b/internal/api/account_handlers.go @@ -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) +} diff --git a/internal/api/block_handlers.go b/internal/api/block_handlers.go new file mode 100644 index 0000000..f9085d8 --- /dev/null +++ b/internal/api/block_handlers.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/dao_api.go b/internal/api/dao_api.go new file mode 100644 index 0000000..1ae8396 --- /dev/null +++ b/internal/api/dao_api.go @@ -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) \ No newline at end of file diff --git a/internal/api/dao_handlers.go b/internal/api/dao_handlers.go new file mode 100644 index 0000000..d895bdd --- /dev/null +++ b/internal/api/dao_handlers.go @@ -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 +} \ No newline at end of file diff --git a/internal/api/handler.go b/internal/api/handler.go new file mode 100644 index 0000000..bb03716 --- /dev/null +++ b/internal/api/handler.go @@ -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(), + } +} \ No newline at end of file diff --git a/internal/api/health.go b/internal/api/health.go new file mode 100644 index 0000000..b00d846 --- /dev/null +++ b/internal/api/health.go @@ -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")) +} \ No newline at end of file diff --git a/internal/api/heartbeat_handler.go b/internal/api/heartbeat_handler.go new file mode 100644 index 0000000..7506937 --- /dev/null +++ b/internal/api/heartbeat_handler.go @@ -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")) +} diff --git a/internal/api/liveness.go b/internal/api/liveness.go new file mode 100644 index 0000000..4678463 --- /dev/null +++ b/internal/api/liveness.go @@ -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")) +} \ No newline at end of file diff --git a/internal/api/middleware.go b/internal/api/middleware.go new file mode 100644 index 0000000..5cf54bc --- /dev/null +++ b/internal/api/middleware.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/router.go b/internal/api/router.go new file mode 100644 index 0000000..ff06b33 --- /dev/null +++ b/internal/api/router.go @@ -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 +} \ No newline at end of file diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..09a87e1 --- /dev/null +++ b/internal/api/server.go @@ -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 +} \ No newline at end of file diff --git a/internal/api/staking_handlers.go b/internal/api/staking_handlers.go new file mode 100644 index 0000000..0063e62 --- /dev/null +++ b/internal/api/staking_handlers.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/tx_handlers.go b/internal/api/tx_handlers.go new file mode 100644 index 0000000..9bb08a4 --- /dev/null +++ b/internal/api/tx_handlers.go @@ -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"}) +} \ No newline at end of file diff --git a/internal/api/utils.go b/internal/api/utils.go new file mode 100644 index 0000000..d1793e0 --- /dev/null +++ b/internal/api/utils.go @@ -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, + }) +} \ No newline at end of file diff --git a/internal/blockchain/block.go b/internal/blockchain/block.go new file mode 100644 index 0000000..fa5c1e3 --- /dev/null +++ b/internal/blockchain/block.go @@ -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 +} \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d253736 --- /dev/null +++ b/internal/config/config.go @@ -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{} +} \ No newline at end of file diff --git a/internal/consensus/consensus.go b/internal/consensus/consensus.go new file mode 100644 index 0000000..e905d7d --- /dev/null +++ b/internal/consensus/consensus.go @@ -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 +} \ No newline at end of file diff --git a/internal/consensus/engine.go b/internal/consensus/engine.go new file mode 100644 index 0000000..06d4898 --- /dev/null +++ b/internal/consensus/engine.go @@ -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 +} \ No newline at end of file diff --git a/internal/consensus/finality.go b/internal/consensus/finality.go new file mode 100644 index 0000000..7810643 --- /dev/null +++ b/internal/consensus/finality.go @@ -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) +} \ No newline at end of file diff --git a/internal/consensus/liveness.go b/internal/consensus/liveness.go new file mode 100644 index 0000000..6ff5d7f --- /dev/null +++ b/internal/consensus/liveness.go @@ -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] +} \ No newline at end of file diff --git a/internal/consensus/messages.go b/internal/consensus/messages.go new file mode 100644 index 0000000..5ea6278 --- /dev/null +++ b/internal/consensus/messages.go @@ -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 +} \ No newline at end of file diff --git a/internal/consensus/network.go b/internal/consensus/network.go new file mode 100644 index 0000000..b593a22 --- /dev/null +++ b/internal/consensus/network.go @@ -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 + } +} \ No newline at end of file diff --git a/internal/consensus/pos.go b/internal/consensus/pos.go new file mode 100644 index 0000000..30af532 --- /dev/null +++ b/internal/consensus/pos.go @@ -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 +} \ No newline at end of file diff --git a/internal/consensus/proposer.go b/internal/consensus/proposer.go new file mode 100644 index 0000000..9280ade --- /dev/null +++ b/internal/consensus/proposer.go @@ -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 +} diff --git a/internal/consensus/quorum.go b/internal/consensus/quorum.go new file mode 100644 index 0000000..66487c2 --- /dev/null +++ b/internal/consensus/quorum.go @@ -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, "" +} \ No newline at end of file diff --git a/internal/consensus/round_loop.go b/internal/consensus/round_loop.go new file mode 100644 index 0000000..1412d0d --- /dev/null +++ b/internal/consensus/round_loop.go @@ -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() + } + } +} \ No newline at end of file diff --git a/internal/consensus/simple/finality.go b/internal/consensus/simple/finality.go new file mode 100644 index 0000000..aafb56a --- /dev/null +++ b/internal/consensus/simple/finality.go @@ -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) +} \ No newline at end of file diff --git a/internal/consensus/simple/simple.go b/internal/consensus/simple/simple.go new file mode 100644 index 0000000..d415505 --- /dev/null +++ b/internal/consensus/simple/simple.go @@ -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" +} \ No newline at end of file diff --git a/internal/consensus/slash.go b/internal/consensus/slash.go new file mode 100644 index 0000000..519dcae --- /dev/null +++ b/internal/consensus/slash.go @@ -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") +} \ No newline at end of file diff --git a/internal/consensus/state.go b/internal/consensus/state.go new file mode 100644 index 0000000..3452c4d --- /dev/null +++ b/internal/consensus/state.go @@ -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() +} \ No newline at end of file diff --git a/internal/consensus/transport_http.go b/internal/consensus/transport_http.go new file mode 100644 index 0000000..c58c5bf --- /dev/null +++ b/internal/consensus/transport_http.go @@ -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 +} \ No newline at end of file diff --git a/internal/consensus/validator_set.go b/internal/consensus/validator_set.go new file mode 100644 index 0000000..0e0f8c8 --- /dev/null +++ b/internal/consensus/validator_set.go @@ -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 +} \ No newline at end of file diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..767c497 --- /dev/null +++ b/internal/crypto/crypto.go @@ -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) +} diff --git a/internal/crypto/keys.go b/internal/crypto/keys.go new file mode 100644 index 0000000..e9eb085 --- /dev/null +++ b/internal/crypto/keys.go @@ -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 \ No newline at end of file diff --git a/internal/crypto/pq_signer.go b/internal/crypto/pq_signer.go new file mode 100644 index 0000000..7d51aba --- /dev/null +++ b/internal/crypto/pq_signer.go @@ -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{}) +} \ No newline at end of file diff --git a/internal/crypto/signer.go b/internal/crypto/signer.go new file mode 100644 index 0000000..3ff1510 --- /dev/null +++ b/internal/crypto/signer.go @@ -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") +} \ No newline at end of file diff --git a/internal/dao/proposal.go b/internal/dao/proposal.go new file mode 100644 index 0000000..435cb84 --- /dev/null +++ b/internal/dao/proposal.go @@ -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 + } +} \ No newline at end of file diff --git a/internal/dao/store.go b/internal/dao/store.go new file mode 100644 index 0000000..311864f --- /dev/null +++ b/internal/dao/store.go @@ -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) +} \ No newline at end of file diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go new file mode 100644 index 0000000..76ba7e2 --- /dev/null +++ b/internal/mempool/mempool.go @@ -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{} +} diff --git a/internal/miner/miner.go b/internal/miner/miner.go new file mode 100644 index 0000000..212d1b2 --- /dev/null +++ b/internal/miner/miner.go @@ -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 +} \ No newline at end of file diff --git a/internal/monitor/heartbeat.go b/internal/monitor/heartbeat.go new file mode 100644 index 0000000..7191150 --- /dev/null +++ b/internal/monitor/heartbeat.go @@ -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] +} \ No newline at end of file diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go new file mode 100644 index 0000000..1ccbcce --- /dev/null +++ b/internal/monitor/monitor.go @@ -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] +} diff --git a/internal/monitoring/metrics.go b/internal/monitoring/metrics.go new file mode 100644 index 0000000..039a7e4 --- /dev/null +++ b/internal/monitoring/metrics.go @@ -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() +} \ No newline at end of file diff --git a/internal/oracle/fetcher.go b/internal/oracle/fetcher.go new file mode 100644 index 0000000..b71c771 --- /dev/null +++ b/internal/oracle/fetcher.go @@ -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 +} \ No newline at end of file diff --git a/internal/oracle/fetcher_test.go b/internal/oracle/fetcher_test.go new file mode 100644 index 0000000..8d3f587 --- /dev/null +++ b/internal/oracle/fetcher_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/oracle/store.go b/internal/oracle/store.go new file mode 100644 index 0000000..f1f4165 --- /dev/null +++ b/internal/oracle/store.go @@ -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] +} \ No newline at end of file diff --git a/internal/oracle/store_test.go b/internal/oracle/store_test.go new file mode 100644 index 0000000..2b6b457 --- /dev/null +++ b/internal/oracle/store_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/oracle/validator.go b/internal/oracle/validator.go new file mode 100644 index 0000000..c34ea9c --- /dev/null +++ b/internal/oracle/validator.go @@ -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") +} \ No newline at end of file diff --git a/internal/oracle/validator_test.go b/internal/oracle/validator_test.go new file mode 100644 index 0000000..73ba136 --- /dev/null +++ b/internal/oracle/validator_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/p2p/dht.go b/internal/p2p/dht.go new file mode 100644 index 0000000..649448c --- /dev/null +++ b/internal/p2p/dht.go @@ -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 +} \ No newline at end of file diff --git a/internal/p2p/host.go b/internal/p2p/host.go new file mode 100644 index 0000000..5b0c8fa --- /dev/null +++ b/internal/p2p/host.go @@ -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) {} \ No newline at end of file diff --git a/internal/p2p/limiter.go b/internal/p2p/limiter.go new file mode 100644 index 0000000..a0a24ca --- /dev/null +++ b/internal/p2p/limiter.go @@ -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) + } + } +} \ No newline at end of file diff --git a/internal/p2p/network.go b/internal/p2p/network.go new file mode 100644 index 0000000..b07ce12 --- /dev/null +++ b/internal/p2p/network.go @@ -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 +} \ No newline at end of file diff --git a/internal/p2p/p2p.go b/internal/p2p/p2p.go new file mode 100644 index 0000000..ae30384 --- /dev/null +++ b/internal/p2p/p2p.go @@ -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() + } +} \ No newline at end of file diff --git a/internal/p2p/p2p_test.go b/internal/p2p/p2p_test.go new file mode 100644 index 0000000..2ed1dbf --- /dev/null +++ b/internal/p2p/p2p_test.go @@ -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) +} \ No newline at end of file diff --git a/internal/staking/rewards.go b/internal/staking/rewards.go new file mode 100644 index 0000000..64b6851 --- /dev/null +++ b/internal/staking/rewards.go @@ -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)) +} \ No newline at end of file diff --git a/internal/staking/store.go b/internal/staking/store.go new file mode 100644 index 0000000..bd76af4 --- /dev/null +++ b/internal/staking/store.go @@ -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) +} \ No newline at end of file diff --git a/internal/state/apply.go b/internal/state/apply.go new file mode 100644 index 0000000..e5be62c --- /dev/null +++ b/internal/state/apply.go @@ -0,0 +1,8 @@ +package state + +import ( + "dejo_node/internal/staking" +) + +var GlobalState = NewState() +var GlobalStakingStore = staking.NewStakingStore() \ No newline at end of file diff --git a/internal/state/domain.go b/internal/state/domain.go new file mode 100644 index 0000000..839fc11 --- /dev/null +++ b/internal/state/domain.go @@ -0,0 +1,7 @@ +package state + +func (s *State) GetBalance(addr string) uint64 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Balances[addr] +} \ No newline at end of file diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..d7d4439 --- /dev/null +++ b/internal/state/state.go @@ -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) +} \ No newline at end of file diff --git a/internal/storage/snapshot.go b/internal/storage/snapshot.go new file mode 100644 index 0000000..5b94ea2 --- /dev/null +++ b/internal/storage/snapshot.go @@ -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) +} \ No newline at end of file diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..d229890 --- /dev/null +++ b/internal/storage/storage.go @@ -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 +} \ No newline at end of file diff --git a/internal/storage/store.go b/internal/storage/store.go new file mode 100644 index 0000000..73a62c8 --- /dev/null +++ b/internal/storage/store.go @@ -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() +} \ No newline at end of file diff --git a/internal/sync/height.go b/internal/sync/height.go new file mode 100644 index 0000000..84678db --- /dev/null +++ b/internal/sync/height.go @@ -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) + } + } +} \ No newline at end of file diff --git a/internal/sync/sync.go b/internal/sync/sync.go new file mode 100644 index 0000000..9051a6b --- /dev/null +++ b/internal/sync/sync.go @@ -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 +} \ No newline at end of file diff --git a/internal/transactions/block.go b/internal/transactions/block.go new file mode 100644 index 0000000..805226a --- /dev/null +++ b/internal/transactions/block.go @@ -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() +} \ No newline at end of file diff --git a/internal/transactions/block_test.go b/internal/transactions/block_test.go new file mode 100644 index 0000000..3e51552 --- /dev/null +++ b/internal/transactions/block_test.go @@ -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)) + } +} \ No newline at end of file diff --git a/internal/transactions/mempool.go b/internal/transactions/mempool.go new file mode 100644 index 0000000..68bfe58 --- /dev/null +++ b/internal/transactions/mempool.go @@ -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 +} \ No newline at end of file diff --git a/internal/transactions/mempool_test.go b/internal/transactions/mempool_test.go new file mode 100644 index 0000000..7e85a63 --- /dev/null +++ b/internal/transactions/mempool_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/transactions/replay.go b/internal/transactions/replay.go new file mode 100644 index 0000000..5cdb1c4 --- /dev/null +++ b/internal/transactions/replay.go @@ -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{}{} +} \ No newline at end of file diff --git a/internal/transactions/transaction.go b/internal/transactions/transaction.go new file mode 100644 index 0000000..1df0efb --- /dev/null +++ b/internal/transactions/transaction.go @@ -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 +} \ No newline at end of file diff --git a/internal/transactions/transaction_test.go b/internal/transactions/transaction_test.go new file mode 100644 index 0000000..f7794e8 --- /dev/null +++ b/internal/transactions/transaction_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/transactions/validation.go b/internal/transactions/validation.go new file mode 100644 index 0000000..2192b2e --- /dev/null +++ b/internal/transactions/validation.go @@ -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) +} \ No newline at end of file diff --git a/internal/transactions/validation_test.go b/internal/transactions/validation_test.go new file mode 100644 index 0000000..9eabfc5 --- /dev/null +++ b/internal/transactions/validation_test.go @@ -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") + } +} \ No newline at end of file diff --git a/internal/ws/events.go b/internal/ws/events.go new file mode 100644 index 0000000..8e2adc5 --- /dev/null +++ b/internal/ws/events.go @@ -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 +} \ No newline at end of file diff --git a/internal/ws/hub.go b/internal/ws/hub.go new file mode 100644 index 0000000..caf4354 --- /dev/null +++ b/internal/ws/hub.go @@ -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") +} \ No newline at end of file diff --git a/main b/main new file mode 100755 index 0000000000000000000000000000000000000000..1dc9490aaf7ccfd3e61a05370c6df21d15deb640 GIT binary patch literal 12452162 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