commit 8f04473c0b1766e554d41318a3c26b189c864133 Author: Júnior Date: Fri May 23 10:44:32 2025 -0300 commit inicial do projeto 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 0000000..2ec7ad1 Binary files /dev/null and b/data/staking.db differ diff --git a/data/state.gob b/data/state.gob new file mode 100644 index 0000000..6f4cb9f Binary files /dev/null and b/data/state.gob differ diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..91f46d8 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,95 @@ +# 🌐 **Documentação da API - DEJO Node** + +## 📌 **Visão Geral** +O **DEJO Node** expõe uma API RPC/WebSockets para permitir **interação com a blockchain**, incluindo envio de transações, consulta de blocos e eventos em tempo real. + +--- + +## **1️⃣ Endpoints REST** + +### **1.1 - Consultar um Bloco** +**GET /block/{id}** +- Retorna informações sobre um bloco específico. + +📌 **Exemplo de resposta:** +```json +{ + "blockHeight": 10234, + "hash": "0xa7b9c...", + "previousHash": "0x9f8a...", + "timestamp": 1712456789, + "transactions": ["0xabc123", "0xdef456"] +} +``` + +### **1.2 - Enviar uma Transação** +**POST /tx** +- Envia uma nova transação para a rede. + +📌 **Exemplo de requisição:** +```json +{ + "from": "0x123...", + "to": "0xabc...", + "value": 100, + "gas": 21000, + "signature": "0x9f8a..." +} +``` + +📌 **Exemplo de resposta:** +```json +{ + "status": "pending", + "txHash": "0xdef789..." +} +``` + +### **1.3 - Consultar Status de uma Transação** +**GET /tx/{hash}** +- Retorna o status de uma transação específica. + +📌 **Exemplo de resposta:** +```json +{ + "txHash": "0xdef789...", + "status": "confirmed", + "blockHeight": 10235 +} +``` + +--- + +## **2️⃣ WebSockets para Eventos em Tempo Real** + +### **2.1 - Subscribing em Novos Blocos** +**WS /events** +- Notifica sempre que um novo bloco é minerado. + +📌 **Exemplo de mensagem recebida:** +```json +{ + "event": "newBlock", + "blockHeight": 10236, + "hash": "0xbbc345..." +} +``` + +### **2.2 - Subscribing em Transações Confirmadas** +**WS /events** +- Notifica quando uma transação específica é confirmada. + +📌 **Exemplo de mensagem recebida:** +```json +{ + "event": "txConfirmed", + "txHash": "0xdef789...", + "blockHeight": 10235 +} +``` + +--- + +📌 **Este documento será atualizado conforme novos endpoints forem adicionados.** + +🚀 **DEJO Node: API eficiente para interação com a blockchain!** 🔥 \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a751789 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,37 @@ +# 🏗️ **Arquitetura do DEJO Node** + +## 📌 **Visão Geral** +O **DEJO Node** é o componente central da blockchain DEJO, responsável por **processar transações, validar blocos, manter consenso, armazenar dados e fornecer uma API para interação externa**. Sua arquitetura modular foi projetada para garantir **segurança, escalabilidade e interoperabilidade**. + +--- + +## **1️⃣ Componentes Principais** +### **1.1 - Camada de Consenso** +- Implementa **Proof-of-Stake (PoS)** ou **Byzantine Fault Tolerance (BFT)** para validar blocos e garantir a integridade da rede. +- Gerencia **eleição de validadores** e mecanismo de **staking**. +- Inclui penalização (slashing) para validadores maliciosos. + +### **1.2 - Camada de Execução de Transações** +- Responsável por **verificar, validar e executar transações**. +- Garante que apenas transações assinadas e válidas sejam incluídas nos blocos. +- Mantém um **mempool** para armazenar transações pendentes. + +### **1.3 - Armazenamento e Estado** +- **LevelDB/BadgerDB** para armazenamento persistente de blocos e estados de conta. +- Suporte a **indexação rápida** para consultas eficientes. +- Implementação de **snapshots periódicos** para recuperação rápida da blockchain. + +### **1.4 - Comunicação P2P** +- Usa **libp2p** para **sincronização de blocos e transações** entre nós. +- Implementa **descoberta dinâmica de peers** e mecanismos de propagação segura. + +### **1.5 - API RPC/WebSockets** +- Disponibiliza **endpoints para consulta e envio de transações**. +- Implementa **WebSockets** para notificações em tempo real. +- Garante **autenticação e controle de acesso** para maior segurança. + +--- + +📌 **Este documento será atualizado conforme novos módulos forem implementados.** + +🚀 **DEJO Node: Arquitetura robusta e preparada para o futuro!** 🔥 \ No newline at end of file diff --git a/docs/consensus.md b/docs/consensus.md new file mode 100644 index 0000000..a4e276f --- /dev/null +++ b/docs/consensus.md @@ -0,0 +1,61 @@ +# ⚖️ Mecanismo de Consenso no DEJO Node + +## 📌 Visão Geral +O **DEJO Node** utiliza um mecanismo de consenso híbrido, suportando **Proof-of-Stake (PoS)** e **Byzantine Fault Tolerance (BFT)**. O objetivo é garantir **segurança, escalabilidade e eficiência** na validação dos blocos e transações. + +--- + +## 🔹 Modelos de Consenso Suportados +### **1. Proof-of-Stake (PoS)** +- Validadores são escolhidos com base na **quantidade de tokens em staking**. +- Mais stake = maior chance de ser escolhido para validar blocos. +- Implementação de **mecanismo de penalização (slashing)** para validadores maliciosos. +- Finalização de blocos sem necessidade de mineração intensiva. + +### **2. Byzantine Fault Tolerance (BFT)** +- Algoritmo que permite consenso mesmo com **até 1/3 dos nós maliciosos**. +- Comunicação direta entre validadores para garantir consenso. +- Tempo de confirmação de bloco reduzido. +- Melhor aplicabilidade para redes permissionadas. + +--- + +## 🔄 Processo de Validação de Blocos +1. Novas transações são propagadas via **rede P2P**. +2. Nós validadores selecionam um conjunto de transações e montam um bloco. +3. O bloco é proposto e enviado para os outros validadores. +4. Dependendo do modelo de consenso: + - **PoS** → O validador líder assina e propaga o bloco. + - **BFT** → Todos os validadores participam da decisão. +5. Após o consenso, o bloco é **finalizado e registrado no banco de dados**. +6. A blockchain se mantém sincronizada via comunicação P2P. + +--- + +## ⚠️ Segurança e Penalizações (Slashing) +Para evitar ataques e validar a integridade da rede, aplicamos penalizações: +- **Slashing parcial**: Redução de stake caso um validador tente validar blocos conflitantes. +- **Slashing total**: Remoção completa do validador em casos de fraude comprovada. +- **Lista negra**: Impedimento de participação futura de validadores maliciosos. + +--- + +## 📂 Estrutura de Diretórios Relacionada +``` +dejo-node/ +│── internal/ +│ ├── consensus/ # Implementação do consenso (PoS/BFT) +│ ├── transactions/ # Processamento de transações +│ ├── storage/ # Persistência dos blocos +│── tests/ +│ ├── consensus/ # Testes unitários e de integração do consenso +``` + +--- + +## 📖 Referências +- [Whitepaper Técnico da DEJO](../Whitepaper_Tecnico.pdf) +- [Plano de Negócios - DEJO Digital Assets](../Plano_de_Negocios.pdf) +- [Arquitetura do DEJO Node](architecture.md) + +🚀 **DEJO Node: Segurança e Consenso para a Blockchain!** 🔥 \ No newline at end of file diff --git a/docs/healthchecks.md b/docs/healthchecks.md new file mode 100644 index 0000000..f210668 --- /dev/null +++ b/docs/healthchecks.md @@ -0,0 +1,108 @@ +# 🔍 **Healthchecks - DEJO Node** + +## 📌 **Visão Geral** +Os healthchecks do **DEJO Node** garantem que a aplicação está rodando corretamente e que seus componentes essenciais estão operacionais. Eles são divididos em três categorias principais: + +1️⃣ **Liveness Probe** → Verifica se o processo ainda está ativo. +2️⃣ **Readiness Probe** → Indica se o serviço pode receber tráfego. +3️⃣ **Startup Probe** → Confirma se a aplicação inicializou corretamente. + +--- + +## **1️⃣ Endpoints de Healthcheck** + +### **1.1 - Liveness Probe** +📌 **Endpoint:** `GET /health/liveness` +- Retorna se o processo do nó está ativo e não travou. +- Se falhar, o Kubernetes pode reiniciar o pod. + +📌 **Exemplo de resposta:** +```json +{ + "status": "ok", + "message": "Node is alive", + "timestamp": 1712456789 +} +``` + +--- + +### **1.2 - Readiness Probe** +📌 **Endpoint:** `GET /health/readiness` +- Verifica se o nó está pronto para receber tráfego. +- Avalia conexões com banco de dados, consenso e rede P2P. + +📌 **Exemplo de resposta (sucesso):** +```json +{ + "status": "ready", + "dependencies": { + "database": "ok", + "consensus": "ok", + "p2p": "ok" + }, + "timestamp": 1712456789 +} +``` + +📌 **Exemplo de resposta (falha):** +```json +{ + "status": "not_ready", + "dependencies": { + "database": "error", + "consensus": "ok", + "p2p": "ok" + }, + "timestamp": 1712456789 +} +``` + +--- + +### **1.3 - Startup Probe** +📌 **Endpoint:** `GET /health/startup` +- Indica se o nó foi iniciado corretamente. +- Útil para evitar que o Kubernetes mate o pod prematuramente. + +📌 **Exemplo de resposta:** +```json +{ + "status": "initialized", + "message": "Node startup completed", + "timestamp": 1712456789 +} +``` + +--- + +## **2️⃣ Configuração no Kubernetes** +📌 **Exemplo de configuração no `deployment.yaml`**: +```yaml +livenessProbe: + httpGet: + path: /health/liveness + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 5 + +readinessProbe: + httpGet: + path: /health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + +startupProbe: + httpGet: + path: /health/startup + port: 8080 + failureThreshold: 30 + periodSeconds: 10 +``` + +--- + +📌 **Este documento será atualizado conforme a evolução do serviço.** + +🚀 **DEJO Node: Garantindo disponibilidade com healthchecks inteligentes!** 🔥 \ No newline at end of file diff --git a/docs/kubernetes.md b/docs/kubernetes.md new file mode 100644 index 0000000..cb76222 --- /dev/null +++ b/docs/kubernetes.md @@ -0,0 +1,131 @@ +# ☁️ **Deploy no Kubernetes - DEJO Node** + +## 📌 **Visão Geral** +Este documento descreve como implantar e gerenciar o **DEJO Node** em um ambiente Kubernetes, garantindo **alta disponibilidade, escalabilidade e monitoramento eficiente**. + +--- + +## **1️⃣ Estrutura de Deploy** + +### **1.1 - Componentes no Kubernetes** +- **Deployment**: Gerencia os pods do DEJO Node. +- **Service**: Exposição dos serviços via ClusterIP, NodePort ou LoadBalancer. +- **ConfigMap**: Armazena configurações como variáveis de ambiente. +- **PersistentVolume (PV) e PersistentVolumeClaim (PVC)**: Garante persistência dos dados da blockchain. +- **Horizontal Pod Autoscaler (HPA)**: Ajusta automaticamente a quantidade de pods com base na carga. + +--- + +## **2️⃣ Configuração do Deployment** +📌 **Exemplo de `deployment.yaml`**: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dejo-node +spec: + replicas: 3 + selector: + matchLabels: + app: dejo-node + template: + metadata: + labels: + app: dejo-node + spec: + containers: + - name: dejo-node + image: dejo/node:latest + ports: + - containerPort: 8545 # Porta RPC + - containerPort: 30303 # Porta P2P + envFrom: + - configMapRef: + name: dejo-config + volumeMounts: + - name: blockchain-storage + mountPath: /data + volumes: + - name: blockchain-storage + persistentVolumeClaim: + claimName: dejo-pvc +``` + +--- + +## **3️⃣ Exposição do Serviço** +📌 **Exemplo de `service.yaml`**: +```yaml +apiVersion: v1 +kind: Service +metadata: + name: dejo-service +spec: + type: LoadBalancer + ports: + - port: 8545 + targetPort: 8545 + protocol: TCP + name: rpc + - port: 30303 + targetPort: 30303 + protocol: TCP + name: p2p + selector: + app: dejo-node +``` + +--- + +## **4️⃣ Escalabilidade com HPA** +📌 **Exemplo de `hpa.yaml`**: +```yaml +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: dejo-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: dejo-node + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +``` + +--- + +## **5️⃣ Monitoramento e Logging** + +### **5.1 - Monitoramento com Prometheus & Grafana** +📌 **Exemplo de configuração do Prometheus**: +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: dejo-monitor +spec: + selector: + matchLabels: + app: dejo-node + endpoints: + - port: metrics + interval: 10s +``` + +### **5.2 - Logging com Fluentd e Elastic Stack** +- Logs coletados com **Fluentd** e enviados para **Elasticsearch**. +- Dashboards criados no **Kibana** para análise em tempo real. + +--- + +📌 **Este documento será atualizado conforme novas estratégias forem implementadas.** + +🚀 **DEJO Node: Kubernetes otimizado para escalabilidade e resiliência!** 🔥 \ No newline at end of file diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..7ce6673 --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,91 @@ +# 📜 **Logging - DEJO Node** + +## 📌 **Visão Geral** +O **DEJO Node** implementa um sistema de logging estruturado para **monitoramento, auditoria e depuração** do funcionamento da blockchain. Utilizamos **Zap (Uber-go)** para logs de alto desempenho e baixa latência. + +--- + +## **1️⃣ Níveis de Log** +Os logs seguem uma hierarquia de severidade: +- **DEBUG** → Informações detalhadas para desenvolvimento. +- **INFO** → Eventos operacionais normais. +- **WARN** → Eventos que podem exigir atenção. +- **ERROR** → Problemas que impactam a funcionalidade, mas não interrompem o serviço. +- **FATAL** → Falhas críticas que resultam no encerramento do serviço. + +--- + +## **2️⃣ Exemplo de Configuração de Logging** +📌 **Trecho do `config.yaml`**: +```yaml +logging: + level: "info" + output: "stdout" # Pode ser "stdout", "file" ou "json" + filePath: "/var/log/dejo-node.log" + format: "json" # Pode ser "json" ou "console" +``` + +📌 **Trecho de código usando Zap:** +```go +package logger + +import ( + "go.uber.org/zap" +) + +var log *zap.Logger + +func InitLogger(level string) { + var err error + cfg := zap.NewProductionConfig() + cfg.Level = zap.NewAtomicLevelAt(parseLogLevel(level)) + log, err = cfg.Build() + if err != nil { + panic("Erro ao inicializar logger: " + err.Error()) + } +} + +func Info(msg string, fields ...zap.Field) { + log.Info(msg, fields...) +} + +func Error(msg string, fields ...zap.Field) { + log.Error(msg, fields...) +} + +func Fatal(msg string, fields ...zap.Field) { + log.Fatal(msg, fields...) +} +``` + +--- + +## **3️⃣ Exemplo de Saída de Log** +📌 **Formato JSON:** +```json +{ + "level": "info", + "timestamp": "2025-03-18T12:00:00Z", + "message": "Novo bloco minerado", + "blockHeight": 10234, + "hash": "0xa7b9c..." +} +``` + +📌 **Formato Console:** +``` +[INFO] 2025-03-18T12:00:00Z Novo bloco minerado | blockHeight=10234 hash=0xa7b9c... +``` + +--- + +## **4️⃣ Integração com Monitoramento** +- Logs podem ser enviados para **Elastic Stack (Elasticsearch + Kibana)**. +- Suporte a **Fluentd** para roteamento de logs para múltiplos destinos. +- **Prometheus e Grafana** podem coletar métricas baseadas nos logs. + +--- + +📌 **Este documento será atualizado conforme novas funcionalidades forem adicionadas.** + +🚀 **DEJO Node: Logging estruturado para máxima observabilidade!** 🔥 \ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..a3bcacf --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,90 @@ +openapi: 3.0.3 +info: + title: DEJO Node API + description: API REST pública do nó da blockchain DEJO + version: 1.0.0 + +paths: + /health: + get: + summary: Verifica se o serviço está vivo + responses: + '200': + description: OK + + /startup: + get: + summary: Verifica se o serviço foi inicializado + responses: + '200': + description: Serviço iniciado + '503': + description: Inicialização incompleta + + /ready: + get: + summary: Verifica se o nó está pronto para receber requisições + responses: + '200': + description: Pronto para uso + '503': + description: Store ou Mempool indisponível + + /tx: + post: + summary: Envia uma nova transação para a rede + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + from: + type: string + to: + type: string + value: + type: number + nonce: + type: integer + gas: + type: integer + signature: + type: string + responses: + '200': + description: Transação enviada + + /block/{hash}: + get: + summary: Retorna um bloco pelo hash + parameters: + - name: hash + in: path + required: true + schema: + type: string + responses: + '200': + description: Dados do bloco + + /tx/{hash}: + get: + summary: Retorna uma transação pelo hash + parameters: + - name: hash + in: path + required: true + schema: + type: string + responses: + '200': + description: Dados da transação + + /mempool: + get: + summary: Retorna as transações pendentes + responses: + '200': + description: Lista de transações pendentes \ No newline at end of file diff --git a/docs/planning.md b/docs/planning.md new file mode 100644 index 0000000..82bff5e --- /dev/null +++ b/docs/planning.md @@ -0,0 +1,92 @@ +# 🏗️ **Plano de Desenvolvimento - DEJO Node** + +## 📌 **Visão Geral** +Este documento define o planejamento das tarefas para o desenvolvimento do **DEJO Node**, garantindo um roadmap estruturado para a implementação dos componentes essenciais. + +--- + +## **1️⃣ Criação da Estrutura do Projeto** +- **1.1 - Definir arquitetura de diretórios e módulos** ✅ **[OK]** +- **1.2 - Criar estrutura base do projeto (pastas e arquivos principais)** ✅ **[OK]** +- **1.3 - Configurar Makefile para build, testes e deploy** ✅ **[OK]** +- **1.4 - Criar Dockerfile otimizado para execução do nó** ✅ **[OK]** +- **1.5 - Configurar variáveis de ambiente e arquivos YAML de configuração** ✅ **[OK]** +- **1.6 - Criar script de inicialização do nó (`cmd/main.go`)** ✅ **[OK]** + +--- + +## **2️⃣ Desenvolvimento do Processamento de Transações** +🔹 **Dependência:** Nenhuma (autocontido, pode ser feito primeiro) +- **2.1 - Criar estrutura de transação (remetente, destinatário, assinatura, gas, etc.)** ✅ +- **2.2 - Implementar validação de transações** ✅ +- **2.3 - Criar sistema de Mempool para transações pendentes** ✅ +- **2.4 - Criar mecanismo de inclusão de transações nos blocos** ✅ + +--- + +## **3️⃣ Armazenamento e Persistência de Dados** +🔹 **Dependência:** Processamento de Transações finalizado +- **3.1 - Definir banco de dados e estrutura de armazenamento (LevelDB/BadgerDB)** ✅ +- **3.2 - Criar estrutura de indexação de blocos e transações** ✅ +- **3.3 - Implementar recuperação rápida de estados da blockchain** ✅ + +--- + +## **4️⃣ Comunicação P2P entre Nós da Rede** +🔹 **Dependência:** Armazenamento e Transações finalizados +- **4.1 - Definir protocolo de comunicação (libp2p, WebSockets)** ✅ +- **4.2 - Implementar descoberta de peers e inicialização da rede** ✅ +- **4.3 - Criar sistema de sincronização de blocos entre nós** ✅ + - **4.3.1 - Criar estrutura do módulo SyncManager** ✅ + - **4.3.2 - Implementar troca de altura (RequestHeight / ResponseHeight)** ✅ + - **4.3.3 - Implementar solicitação de blocos ausentes via stream** ✅ + - **4.3.4 - Implementar envio de blocos em resposta a solicitações** ✅ + - **4.3.5 - Validar blocos recebidos e aplicar no storage local** ✅ + - **4.3.6 - Testes automatizados de sincronização entre dois nós** ✅ + +--- + +## **5️⃣ Implementação do Mecanismo de Consenso** +🔹 **Dependência:** Comunicação P2P pronta +- **5.1 - Definir e implementar o algoritmo de consenso (PoS ou BFT)** ✅ +- **5.2 - Criar mecanismo de finalização de blocos** +- **5.3 - Implementar auditoria e métricas para análise de consenso** + +--- + +## **6️⃣ Desenvolvimento da API RPC/WebSockets** +🔹 **Dependência:** Processamento de Transações e Consenso implementados +- **6.1 - Criar endpoints para consulta de blocos e transações** +- **6.2 - Implementar envio de novas transações via RPC** +- **6.3 - Criar suporte para eventos via WebSockets** +- **6.4 - Adicionar estrutura de resposta JSON detalhada na documentação** + +--- + +## **7️⃣ Implementação da Governança e Staking** +🔹 **Dependência:** Consenso implementado +- **7.1 - Criar sistema de staking para eleição de validadores** +- **7.2 - Implementar contratos de votação on-chain para governança** +- **7.3 - Criar API para registro e consulta de propostas de governança** + +--- + +## **8️⃣ Implementação dos Oráculos e Integração com Dados Externos** +🔹 **Dependência:** API RPC/WebSockets implementada +- **8.1 - Criar integração com provedores de dados externos (ex.: Chainlink, API3)** +- **8.2 - Implementar mecanismo de consenso para validação de dados oraculares** +- **8.3 - Criar sistema de auditoria para dados recebidos de oráculos** + +--- + +## **9️⃣ Sistema de Segurança e Proteção contra Ataques** +🔹 **Dependência:** Consenso implementado +- **9.1 - Implementar proteção contra ataques Sybil e DDoS** +- **9.2 - Criar auditoria e monitoramento de atividades suspeitas** +- **9.3 - Implementar criptografia pós-quântica para segurança futura** + +--- + +📌 **Este documento será atualizado conforme a evolução do projeto.** + +🚀 **DEJO Node: Rumo à construção de uma blockchain robusta e escalável!** 🔥 \ No newline at end of file diff --git a/docs/roadmap_v1.0.0.md b/docs/roadmap_v1.0.0.md new file mode 100644 index 0000000..4726ac9 --- /dev/null +++ b/docs/roadmap_v1.0.0.md @@ -0,0 +1,182 @@ +## 🧩 Roadmap DEJO Node v1.0.0 — Documentação Técnica Operacional + +--- + +### 🧠 1. Execução de Transações no Bloco + +#### **1.1. Estrutura de Contas** +> 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 0000000..1dc9490 Binary files /dev/null and b/main differ diff --git a/project copy.md b/project copy.md new file mode 100644 index 0000000..348761b --- /dev/null +++ b/project copy.md @@ -0,0 +1,80 @@ +# 📌 Visão Geral do Projeto DEJO Node — Checklist Técnico + +--- + +## ✅ COMPLETO + +### 🔁 Engine de Consenso +- [x] Proposal → Prevote → Precommit +- [x] Quórum por stake (PoS BFT) +- [x] Persistência de blocos finalizados + +### 🔐 Staking +- [x] Stake/Unstake via API REST +- [x] Bloqueio por tempo (duração) +- [x] Validação com stake mínimo (`minStake`) +- [x] Persiste em `staking.db` + +### 🗳️ DAO (Governança) +- [x] Propostas com peso por stake +- [x] Votação e apuração com 66% stake +- [x] Tipos: `GENERIC`, `PARAM_CHANGE` +- [x] Execução automática de `PARAM_CHANGE` +- [x] Persiste em `proposals.db` + +### 💾 Blocos e Transações +- [x] Estrutura básica de bloco e tx +- [x] Geração de bloco no consenso + +### 📊 Monitoramento +- [x] Prometheus em `/metrics` + +--- + +## 🟡 EM ANDAMENTO / PARCIAL + +### 🧠 Execução de Transações +- [ ] Aplicar lógica real ao bloco (ex: movimentar saldo) + +### 📚 State Machine Application +- [ ] Módulo separado de estado (contas, variáveis, contratos?) +- [ ] ABCI ou internalização via interface + +### 💸 Recompensas e Penalidades +- [ ] Cálculo de rewards +- [ ] Slashing por inatividade ou falha + +### 🌐 API Avançada +- [ ] Consultas: `/accounts`, `/state`, `/validators`, `/dao/status` + +### 🧪 Testes e Simulações +- [ ] Testes unitários e mocks de múltiplos nós +- [ ] Scripts PoS e DAO automatizados + +--- + +## 🔴 A FAZER + +### 🔗 Rede P2P entre nós +- [ ] Descoberta, broadcast e sync de blocos +- [ ] Comunicação TCP ou gRPC + +### 🔐 Assinaturas Criptográficas +- [ ] Assinaturas reais de votos e transações +- [ ] Validação de chaves públicas + +### 🧬 Genesis e Bootstrapping +- [ ] Arquivo genesis inicial +- [ ] Preload de validadores, config e estado + +### 🧰 CLI para interação +- [ ] `dejo stake`, `dejo tx`, `dejo query` + +--- + +## 💡 Extras (Futuros) +- [ ] Delegação de voto (liquid democracy) +- [ ] DAO `UPGRADE` de rede e parâmetros +- [ ] Inflação, distribuição e queima de tokens +- [ ] Snapshots e sync otimizado +- [ ] Docker / Compose para simulação em rede \ No newline at end of file diff --git a/project.txt b/project.txt new file mode 100644 index 0000000..b46f4bd --- /dev/null +++ b/project.txt @@ -0,0 +1,95 @@ +# 📌 Projeto DEJO Node — Roadmap de Desenvolvimento + +Este documento organiza as etapas do projeto em formato de roadmap, com granularidade suficiente para priorização em sprints. + +--- + +## ✅ ENTREGUE / COMPLETO + +### 🔁 Engine de Consenso +- [x] Ciclo de consenso: Proposal → Prevote → Precommit +- [x] Round e timeout por fase +- [x] Quórum BFT ponderado por stake (PoS) +- [x] Proposer rotativo por altura + +### 🔐 Módulo de Staking +- [x] Registro de stake e unstake via API REST +- [x] Cálculo de bloqueio por duração (Unix time) +- [x] Persistência em `staking.db` + +### 🗳️ Módulo DAO (Governança) +- [x] Criação de propostas via API +- [x] Voto com peso proporcional ao stake +- [x] Fechamento automático por tempo e quorum +- [x] Execução de `PARAM_CHANGE` (altera minStake) + +### 💾 Blocos e Transações +- [x] Estrutura de bloco e transação +- [x] Geração de bloco no consenso +- [x] Hash de bloco e persistência + +### 📊 Monitoramento +- [x] Métricas Prometheus em `/metrics` +- [x] Logs por fase de consenso + +--- + +## 🟡 EM PROGRESSO / PRÓXIMO + +### 🧠 Execução de Blocos e Estado +- [ ] Estrutura de contas (ex: `map[address]Balance`) +- [ ] Aplicar transações que movimentam tokens +- [ ] Atualização de estado no commit do bloco + +### 📚 State Machine (abstração de estado) +- [ ] Criar módulo `internal/state` +- [ ] Interface para aplicar, reverter e consultar estado +- [ ] Encapsular lógica de staking, saldo e DAO + +### 💸 Recompensas e Penalidades +- [ ] Distribuir reward por bloco finalizado (ex: 10 tokens) +- [ ] Atualizar saldo dos validadores automaticamente +- [ ] Penalizar (slash) quem não votar ou agir de forma inválida + +### 🌐 API Avançada +- [ ] `/validators` — status atual dos validadores +- [ ] `/accounts/{addr}` — saldo e staking +- [ ] `/dao/status` — propostas abertas e stats + +### 🧪 Testes e Validação +- [ ] Mocks de múltiplos nós rodando em paralelo +- [ ] Testes unitários de staking e DAO +- [ ] Teste de ciclo completo de proposta → execução + +--- + +## 🔴 A DESENVOLVER + +### 🔗 Rede P2P entre nós +- [ ] Protocolo de descoberta (peer discovery) +- [ ] Broadcast de blocos e votos +- [ ] Sincronismo de blocos (sync/fast-sync) +- [ ] Comunicação TCP/gRPC entre nós + +### 🔐 Assinaturas Criptográficas +- [ ] Geração de chave pública/privada para cada node +- [ ] Assinatura de transações e mensagens +- [ ] Validação de assinaturas nos blocos e mensagens + +### 🧬 Genesis e Bootstrap +- [ ] Arquivo `genesis.json` com config inicial +- [ ] Carga automática de blocos iniciais e parâmetros + +### 🧰 CLI (Interface de Linha de Comando) +- [ ] `dejo stake ...` — registrar stake via terminal +- [ ] `dejo tx send ...` — enviar transação manual +- [ ] `dejo query dao ...` — consultar propostas + +--- + +## 💡 EXTRAS / FUTURO +- [ ] Delegação de voto (liquid democracy) +- [ ] Propostas DAO tipo `UPGRADE` +- [ ] Inflação e distribuição periódica +- [ ] Snapshots de estado + sync otimizado +- [ ] Compose com múltiplos nós simulando rede \ No newline at end of file diff --git a/scripts/start-node-A.sh b/scripts/start-node-A.sh new file mode 100755 index 0000000..05711e4 --- /dev/null +++ b/scripts/start-node-A.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +export NODE_ID=node-A +export DEJO_VALIDATORS=node-A,node-B +export DEJO_PEERS=http://localhost:8082 +export DEJO_PORT=8081 + +echo "🚀 Iniciando node-A na porta 8081..." +go run cmd/main.go \ No newline at end of file diff --git a/scripts/start-node-B.sh b/scripts/start-node-B.sh new file mode 100755 index 0000000..2964dff --- /dev/null +++ b/scripts/start-node-B.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +export NODE_ID=node-B +export DEJO_VALIDATORS=node-A,node-B +export DEJO_PEERS=http://localhost:8081 +export DEJO_PORT=8082 + +echo "🚀 Iniciando node-B na porta 8082..." +go run cmd/main.go \ No newline at end of file