diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..46da22f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Go Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go run cmd/tester/main.go diff --git a/.gitignore b/.gitignore index aaadf73..7afc928 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ +bin +blockchain.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fd53ac3 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# Makefile for Simple BlockChain (SBC) + +# Variables +APP_NAME_CLI = sbc +APP_NAME_DAEMON = sbcd +BUILD_DIR = bin +CMD_CLI_DIR = ./cmd/sbc +CMD_DAEMON_DIR = ./cmd/sbcd + +# Go parameters +GOCMD = go +GOBUILD = $(GOCMD) build +GOCLEAN = $(GOCMD) clean +GOTEST = $(GOCMD) run ./cmd/tester +GOGET = $(GOCMD) get + +.PHONY: all build clean test help cli daemon + +all: build + +build: cli daemon + +cli: + @echo "Building CLI client..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME_CLI) $(CMD_CLI_DIR) + +daemon: + @echo "Building Daemon..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME_DAEMON) $(CMD_DAEMON_DIR) + +clean: + @echo "Cleaning up..." + $(GOCLEAN) + rm -rf $(BUILD_DIR) + +test: + @echo "Running tests..." + $(GOTEST) + +help: + @echo "Available commands:" + @echo " make build - Build both CLI and Daemon" + @echo " make cli - Build the CLI client" + @echo " make daemon - Build the Daemon" + @echo " make clean - Remove built binaries" + @echo " make test - Run Go tests" diff --git a/Tests/background/blockchain_bg_spec.go b/Tests/background/blockchain_bg_spec.go new file mode 100644 index 0000000..74a1ef1 --- /dev/null +++ b/Tests/background/blockchain_bg_spec.go @@ -0,0 +1,37 @@ +package background_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" + "sync" +) + +func init() { + s := gest.Describe("Blockchain Background") + + s.It("should handle rapid block additions", func(t *gest.T) { + bc := blockchain.NewBlockchain() + + var wg sync.WaitGroup + numGoroutines := 10 + blocksPerGoroutine := 10 + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + for j := 0; j < blocksPerGoroutine; j++ { + bc.AddBlock(100 + id*10 + j) + } + }(i) + } + + wg.Wait() + + expectedLength := 1 + (numGoroutines * blocksPerGoroutine) + t.Expect(bc.Length()).ToBe(expectedLength) + t.Expect(bc.IsValid()).ToBeTrue() + }) + + gest.Register(s) +} diff --git a/Tests/background/p2p_bg_spec.go b/Tests/background/p2p_bg_spec.go new file mode 100644 index 0000000..b623472 --- /dev/null +++ b/Tests/background/p2p_bg_spec.go @@ -0,0 +1,38 @@ +package background_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p" + "github.com/caiolandgraf/gest/gest" + "sync" +) + +func init() { + s := gest.Describe("P2P Background") + + s.It("should handle rapid block broadcasts concurrently", func(t *gest.T) { + bc := blockchain.NewBlockchain() + cfg := p2p.DefaultConfig() + cfg.ListenAddr = "/ip4/127.0.0.1/tcp/0" + + server, _ := p2p.NewServer(cfg, bc) + defer server.Close() + + var wg sync.WaitGroup + numBroadcasters := 5 + + for i := 0; i < numBroadcasters; i++ { + wg.Add(1) + go func(bpm int) { + defer wg.Done() + block := blockchain.NewBlock(1, bpm, bc.GetLastBlock().Hash) + server.BroadcastBlock(block) + }(100 + i) + } + + wg.Wait() + t.Expect(len(server.GetPeers())).ToBe(0) // No peers to broadcast to, but should not crash + }) + + gest.Register(s) +} diff --git a/Tests/background/storage_bg_spec.go b/Tests/background/storage_bg_spec.go new file mode 100644 index 0000000..98dabcd --- /dev/null +++ b/Tests/background/storage_bg_spec.go @@ -0,0 +1,44 @@ +package background_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/storage" + "github.com/caiolandgraf/gest/gest" + "os" + "sync" +) + +func init() { + s := gest.Describe("Storage Background") + + testDir := "bg_storage_data" + + s.It("should handle rapid store operations concurrently", func(t *gest.T) { + bc := blockchain.NewBlockchain() + store, _ := storage.NewStore(testDir) + defer func() { + store.Close() + os.RemoveAll(testDir) + }() + + var wg sync.WaitGroup + numOperations := 10 + + for i := 0; i < numOperations; i++ { + wg.Add(1) + go func(bpm int) { + defer wg.Done() + bc.AddBlock(bpm) + store.Save(bc) + }(100 + i) + } + + wg.Wait() + + newBC := blockchain.NewBlockchain() + store.Load(newBC) + t.Expect(newBC.Length()).Not().ToBe(0) + }) + + gest.Register(s) +} diff --git a/Tests/e2e/blockchain_e2e_spec.go b/Tests/e2e/blockchain_e2e_spec.go new file mode 100644 index 0000000..3af1088 --- /dev/null +++ b/Tests/e2e/blockchain_e2e_spec.go @@ -0,0 +1,40 @@ +package e2e_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("Blockchain E2E") + + s.It("should replace chain with longer valid chain", func(t *gest.T) { + bc := blockchain.NewBlockchain() + + // Create a separate longer chain + newBC := blockchain.NewBlockchain() + newBC.AddBlock(100) + newBC.AddBlock(110) + newBC.AddBlock(120) + + longerBlocks := newBC.GetAllBlocks() + + success, msg := bc.TryAcceptChain(longerBlocks) + t.Expect(success).ToBeTrue() + t.Expect(msg).ToBe("chain replaced with longer version") + t.Expect(bc.Length()).ToBe(4) + }) + + s.It("should validate chain with multiple blocks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + bc.AddBlock(110) + + blocks := bc.GetAllBlocks() + valid, msg := bc.ValidateChain(blocks) + t.Expect(valid).ToBeTrue() + t.Expect(msg).ToBe("") + }) + + gest.Register(s) +} diff --git a/Tests/e2e/p2p_e2e_spec.go b/Tests/e2e/p2p_e2e_spec.go new file mode 100644 index 0000000..7472950 --- /dev/null +++ b/Tests/e2e/p2p_e2e_spec.go @@ -0,0 +1,47 @@ +package e2e_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p" + "github.com/caiolandgraf/gest/gest" + "time" +) + +func init() { + s := gest.Describe("P2P E2E") + + s.It("should synchronize blocks between two nodes", func(t *gest.T) { + bc1 := blockchain.NewBlockchain() + bc2 := blockchain.NewBlockchain() + + cfg1 := p2p.DefaultConfig() + cfg1.ListenAddr = "/ip4/127.0.0.1/tcp/0" + + server1, _ := p2p.NewServer(cfg1, bc1) + + cfg2 := p2p.DefaultConfig() + cfg2.ListenAddr = "/ip4/127.0.0.1/tcp/0" + server2, _ := p2p.NewServer(cfg2, bc2) + + // Add block to node 1 + bc1.AddBlock(100) + t.Expect(bc1.Length()).ToBe(2) + + // Manually connect server2 to server1 + addr1 := server1.GetAddrs()[0].String() + "/p2p/" + server1.GetHostID() + err := server2.ConnectToPeer(addr1) + t.Expect(err).ToBeNil() + + // Give some time for the handshake to complete asynchronously + time.Sleep(100 * time.Millisecond) + + // Verify they are connected + t.Expect(len(server1.GetPeers())).Not().ToBe(0) + t.Expect(len(server2.GetPeers())).Not().ToBe(0) + + server1.Close() + server2.Close() + }) + + gest.Register(s) +} diff --git a/Tests/e2e/storage_e2e_spec.go b/Tests/e2e/storage_e2e_spec.go new file mode 100644 index 0000000..d77d72b --- /dev/null +++ b/Tests/e2e/storage_e2e_spec.go @@ -0,0 +1,38 @@ +package e2e_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/storage" + "github.com/caiolandgraf/gest/gest" + "os" +) + +func init() { + s := gest.Describe("Storage E2E") + + testDir := "e2e_storage_data" + + s.It("should store and retrieve a complete chain after restart", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + bc.AddBlock(200) + bc.AddBlock(300) + + store, _ := storage.NewStore(testDir) + store.Save(bc) + store.Close() + + // Simulate restart + newBC := blockchain.NewBlockchain() + newStore, _ := storage.NewStore(testDir) + newStore.Load(newBC) + + t.Expect(newBC.Length()).ToBe(4) + t.Expect(newBC.GetLastBlock().BPM).ToBe(300) + + newStore.Close() + os.RemoveAll(testDir) + }) + + gest.Register(s) +} diff --git a/Tests/unit/blockchain/block_spec.go b/Tests/unit/blockchain/block_spec.go new file mode 100644 index 0000000..f41adb9 --- /dev/null +++ b/Tests/unit/blockchain/block_spec.go @@ -0,0 +1,78 @@ +package blockchain_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "time" + + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("Block") + + s.It("should calculate hash correctly", func(t *gest.T) { + b := blockchain.NewBlock(1, 100, "prev-hash") + expectedHash := b.CalculateHash() + t.Expect(b.Hash).ToBe(expectedHash) + }) + + s.It("should validate valid block", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(1, 100, prev.Hash) + err := b.Validate(prev) + t.Expect(err).ToBeNil() + }) + + s.It("should reject block with invalid hash", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(1, 100, prev.Hash) + b.Hash = "invalid-hash" + err := b.Validate(prev) + t.Expect(err).Not().ToBeNil() + t.Expect(err.Error()).ToBe("invalid hash") + }) + + s.It("should reject block with wrong index", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(2, 100, prev.Hash) // Index should be 1 + err := b.Validate(prev) + t.Expect(err).Not().ToBeNil() + t.Expect(err.Error()).ToBe("invalid index") + }) + + s.It("should reject block with wrong prevHash", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(1, 100, "wrong-prev-hash") + err := b.Validate(prev) + t.Expect(err).Not().ToBeNil() + t.Expect(err.Error()).ToBe("invalid prev hash") + }) + + s.It("should reject block with future timestamp", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(1, 100, prev.Hash) + b.Timestamp = time.Now().Add(1 * time.Hour) + b.Hash = b.CalculateHash() // Re-calculate hash to pass hash check + err := b.Validate(prev) + t.Expect(err).Not().ToBeNil() + t.Expect(err.Error()).ToBe("timestamp too far in future") + }) + + s.It("should reject block with negative BPM", func(t *gest.T) { + prev := blockchain.GenerateGenesisBlock() + b := blockchain.NewBlock(1, -50, prev.Hash) + err := b.Validate(prev) + t.Expect(err).Not().ToBeNil() + t.Expect(err.Error()).ToBe("invalid BPM") + }) + + s.It("should create genesis block correctly", func(t *gest.T) { + genesis := blockchain.GenerateGenesisBlock() + t.Expect(genesis.Index).ToBe(0) + t.Expect(genesis.BPM).ToBe(0) + t.Expect(genesis.PrevHash).ToBe("0") + t.Expect(genesis.Hash).ToBe(genesis.CalculateHash()) + }) + + gest.Register(s) +} diff --git a/Tests/unit/blockchain/blockchain_spec.go b/Tests/unit/blockchain/blockchain_spec.go new file mode 100644 index 0000000..973a77d --- /dev/null +++ b/Tests/unit/blockchain/blockchain_spec.go @@ -0,0 +1,100 @@ +package blockchain_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("Blockchain") + + s.It("should create blockchain with genesis", func(t *gest.T) { + bc := blockchain.NewBlockchain() + t.Expect(bc.Length()).ToBe(1) + t.Expect(bc.GetLastBlock().Index).ToBe(0) + }) + + s.It("should add blocks correctly", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) + t.Expect(bc.Length()).ToBe(2) + t.Expect(bc.GetLastBlock().BPM).ToBe(80) + t.Expect(bc.GetLastBlock().Index).ToBe(1) + }) + + s.It("should process valid blocks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + last := bc.GetLastBlock() + newBlock := blockchain.NewBlock(1, 100, last.Hash) + bc.ProcessBlock(newBlock) + t.Expect(bc.Length()).ToBe(2) + t.Expect(bc.GetLastBlock().Hash).ToBe(newBlock.Hash) + }) + + s.It("should reject invalid blocks in process", func(t *gest.T) { + bc := blockchain.NewBlockchain() + invalidBlock := blockchain.NewBlock(1, 100, "invalid-prev-hash") + bc.ProcessBlock(invalidBlock) + t.Expect(bc.Length()).ToBe(1) // Should remain 1 + }) + + s.It("should subscribe and notify callbacks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + called := false + bc.Subscribe(func(b *blockchain.Block) { + called = true + t.Expect(b.BPM).ToBe(120) + }) + bc.AddBlock(120) + t.Expect(called).ToBe(true) + }) + + s.It("should return last block", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(70) + bc.AddBlock(90) + t.Expect(bc.GetLastBlock().BPM).ToBe(90) + }) + + s.It("should validate valid chain", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) + bc.AddBlock(90) + t.Expect(bc.IsValid()).ToBeTrue() + }) + + s.It("should return all blocks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) + bc.AddBlock(90) + blocks := bc.GetAllBlocks() + t.Expect(len(blocks)).ToBe(3) + t.Expect(blocks[0].Index).ToBe(0) + t.Expect(blocks[1].Index).ToBe(1) + t.Expect(blocks[2].Index).ToBe(2) + }) + + s.It("should find block by hash", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) + last := bc.GetLastBlock() + found := bc.GetBlockByHash(last.Hash) + t.Expect(found).Not().ToBeNil() + t.Expect(found.Hash).ToBe(last.Hash) + }) + + s.It("should get blocks after hash", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) // index 1 + bc.AddBlock(90) // index 2 + bc.AddBlock(100) // index 3 + + genesis := bc.GetAllBlocks()[0] + afterGenesis := bc.GetBlocksAfter(genesis.Hash, 2) + t.Expect(len(afterGenesis)).ToBe(2) + t.Expect(afterGenesis[0].Index).ToBe(1) + t.Expect(afterGenesis[1].Index).ToBe(2) + }) + + gest.Register(s) +} diff --git a/Tests/unit/blockchain/node_spec.go b/Tests/unit/blockchain/node_spec.go new file mode 100644 index 0000000..8be3507 --- /dev/null +++ b/Tests/unit/blockchain/node_spec.go @@ -0,0 +1,37 @@ +package blockchain_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" + "fmt" +) + +func init() { + s := gest.Describe("BlockchainNode") + + s.It("should create node correctly", func(t *gest.T) { + block := blockchain.NewBlock(1, 100, "prev") + node := blockchain.NewBlockchainNode(block) + t.Expect(node).Not().ToBeNil() + }) + + s.It("should format node string", func(t *gest.T) { + block := blockchain.NewBlock(1, 100, "prev") + node := blockchain.NewBlockchainNode(block) + expected := fmt.Sprintf("Block #1 [Hash: %s...]", block.Hash[:8]) + t.Expect(node.String()).ToBe(expected) + }) + + s.It("should link nodes bidirectionally", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(80) + + // Get all blocks to verify sequence + blocks := bc.GetAllBlocks() + t.Expect(len(blocks)).ToBe(2) + t.Expect(blocks[0].Hash).Not().ToBe(blocks[1].Hash) + t.Expect(blocks[1].PrevHash).ToBe(blocks[0].Hash) + }) + + gest.Register(s) +} diff --git a/Tests/unit/p2p/config_spec.go b/Tests/unit/p2p/config_spec.go new file mode 100644 index 0000000..9918d3c --- /dev/null +++ b/Tests/unit/p2p/config_spec.go @@ -0,0 +1,40 @@ +package p2p_tests + +import ( + "KitsuneSemCalda/SBC/internal/p2p" + "github.com/caiolandgraf/gest/gest" + "os" +) + +func init() { + s := gest.Describe("P2P Config") + + s.It("should return a default data directory", func(t *gest.T) { + dir := p2p.DefaultDataDir() + t.Expect(dir).Not().ToBe("") + }) + + s.It("should return default bootstrap nodes", func(t *gest.T) { + nodes := p2p.GetBootstrapNodes() + t.Expect(len(nodes)).ToBe(0) // Default is empty as per current code + }) + + s.It("should return a default config object", func(t *gest.T) { + cfg := p2p.DefaultConfig() + t.Expect(cfg).Not().ToBeNil() + t.Expect(cfg.ListenAddr).ToBe("/ip4/0.0.0.0/tcp/0") + t.Expect(cfg.DiscoveryPort).ToBe(9999) + }) + + s.It("should handle bootstrap nodes from environment variable", func(t *gest.T) { + os.Setenv("SBC_BOOTNODES", "node1,node2") + defer os.Unsetenv("SBC_BOOTNODES") + + nodes := p2p.GetBootstrapNodes() + t.Expect(len(nodes)).ToBe(2) + t.Expect(nodes[0]).ToBe("node1") + t.Expect(nodes[1]).ToBe("node2") + }) + + gest.Register(s) +} diff --git a/Tests/unit/p2p/message_spec.go b/Tests/unit/p2p/message_spec.go new file mode 100644 index 0000000..f82bcc6 --- /dev/null +++ b/Tests/unit/p2p/message_spec.go @@ -0,0 +1,47 @@ +package p2p_tests + +import ( + "KitsuneSemCalda/SBC/internal/p2p" + "github.com/caiolandgraf/gest/gest" + "encoding/json" +) + +func init() { + s := gest.Describe("P2P Message") + + s.It("should create a new message with payload", func(t *gest.T) { + payload := p2p.VersionPayload{Version: 1, BestHeight: 10} + msg, err := p2p.NewMessage(p2p.MsgVersion, payload) + t.Expect(err).ToBeNil() + t.Expect(msg.Type).ToBe(p2p.MsgVersion) + + var decodedPayload p2p.VersionPayload + err = json.Unmarshal(msg.Payload, &decodedPayload) + t.Expect(err).ToBeNil() + t.Expect(decodedPayload.BestHeight).ToBe(10) + }) + + s.It("should encode message to bytes", func(t *gest.T) { + msg := &p2p.Message{Type: p2p.MsgVerAck, Payload: []byte("{}")} + bytes, err := msg.Encode() + t.Expect(err).ToBeNil() + t.Expect(len(bytes)).Not().ToBe(0) + }) + + s.It("should decode message from bytes", func(t *gest.T) { + originalMsg := &p2p.Message{Type: p2p.MsgGetPeers, Payload: []byte("{}")} + bytes, _ := originalMsg.Encode() + + decodedMsg, err := p2p.DecodeMessage(bytes) + t.Expect(err).ToBeNil() + t.Expect(decodedMsg.Type).ToBe(p2p.MsgGetPeers) + }) + + s.It("should handle empty payloads", func(t *gest.T) { + msg, err := p2p.NewMessage(p2p.MsgVerAck, nil) + t.Expect(err).ToBeNil() + t.Expect(msg.Type).ToBe(p2p.MsgVerAck) + }) + + gest.Register(s) +} diff --git a/Tests/unit/p2p/peer_spec.go b/Tests/unit/p2p/peer_spec.go new file mode 100644 index 0000000..7da20fe --- /dev/null +++ b/Tests/unit/p2p/peer_spec.go @@ -0,0 +1,27 @@ +package p2p_tests + +import ( + "KitsuneSemCalda/SBC/internal/p2p" + "github.com/caiolandgraf/gest/gest" + "github.com/libp2p/go-libp2p/core/peer" + "fmt" +) + +func init() { + s := gest.Describe("P2P Peer") + + s.It("should represent peer correctly as string", func(t *gest.T) { + pID, _ := peer.Decode("12D3KooWLGpBKHhshKgGJiiKxKUPKJVBsE3w1td6jSPTquU1xCjk") + p := p2p.NewPeer(nil, pID) + expected := fmt.Sprintf("Peer{%s}", pID) + t.Expect(p.String()).ToBe(expected) + }) + + s.It("should store the correct peer ID", func(t *gest.T) { + pID := peer.ID("test-peer") + p := p2p.NewPeer(nil, pID) + t.Expect(p.ID).ToBe(pID) + }) + + gest.Register(s) +} diff --git a/Tests/unit/sbc/cli_spec.go b/Tests/unit/sbc/cli_spec.go new file mode 100644 index 0000000..8f5ca2b --- /dev/null +++ b/Tests/unit/sbc/cli_spec.go @@ -0,0 +1,25 @@ +package sbc_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("SBC CLI Logic") + + s.It("should validate blockchain via cli logic", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + t.Expect(bc.IsValid()).ToBeTrue() + }) + + s.It("should report correct blockchain length", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + bc.AddBlock(200) + t.Expect(bc.Length()).ToBe(3) + }) + + gest.Register(s) +} diff --git a/Tests/unit/sbcd/daemon_spec.go b/Tests/unit/sbcd/daemon_spec.go new file mode 100644 index 0000000..127bc90 --- /dev/null +++ b/Tests/unit/sbcd/daemon_spec.go @@ -0,0 +1,24 @@ +package sbcd_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("SBCD Daemon Logic") + + s.It("should maintain blockchain integrity in maintenance tasks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + t.Expect(bc.IsValid()).ToBeTrue() + }) + + s.It("should have correct initial state", func(t *gest.T) { + bc := blockchain.NewBlockchain() + t.Expect(bc.Length()).ToBe(1) + t.Expect(bc.GetLastBlock().Index).ToBe(0) + }) + + gest.Register(s) +} diff --git a/Tests/unit/seed/seed_spec.go b/Tests/unit/seed/seed_spec.go new file mode 100644 index 0000000..109b4ac --- /dev/null +++ b/Tests/unit/seed/seed_spec.go @@ -0,0 +1,41 @@ +package seed_tests + +import ( + "github.com/caiolandgraf/gest/gest" + "net/http" + "net/http/httptest" +) + +func init() { + s := gest.Describe("Seed Server Logic") + + s.It("should respond to health endpoint", func(t *gest.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + req, _ := http.NewRequest("GET", "/health", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + t.Expect(rr.Code).ToBe(http.StatusOK) + t.Expect(rr.Body.String()).ToBe("ok") + }) + + s.It("should respond to seeds endpoint with empty list initially", func(t *gest.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"peers":[]}`)) + }) + + req, _ := http.NewRequest("GET", "/seeds", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + t.Expect(rr.Code).ToBe(http.StatusOK) + t.Expect(rr.Body.String()).ToBe(`{"peers":[]}`) + }) + + gest.Register(s) +} diff --git a/Tests/unit/storage/store_spec.go b/Tests/unit/storage/store_spec.go new file mode 100644 index 0000000..874d731 --- /dev/null +++ b/Tests/unit/storage/store_spec.go @@ -0,0 +1,43 @@ +package storage_tests + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/storage" + "github.com/caiolandgraf/gest/gest" + "os" +) + +func init() { + s := gest.Describe("Storage Store") + + testDir := "test_sbc_data" + + s.It("should create store with valid directory", func(t *gest.T) { + store, err := storage.NewStore(testDir) + t.Expect(err).ToBeNil() + t.Expect(store).Not().ToBeNil() + store.Close() + os.RemoveAll(testDir) + }) + + s.It("should save and load blockchain blocks", func(t *gest.T) { + bc := blockchain.NewBlockchain() + bc.AddBlock(100) + bc.AddBlock(200) + + store, _ := storage.NewStore(testDir) + err := store.Save(bc) + t.Expect(err).ToBeNil() + + newBC := blockchain.NewBlockchain() + err = store.Load(newBC) + t.Expect(err).ToBeNil() + t.Expect(newBC.Length()).ToBe(3) + t.Expect(newBC.GetLastBlock().BPM).ToBe(200) + + store.Close() + os.RemoveAll(testDir) + }) + + gest.Register(s) +} diff --git a/cmd/sbc/main.go b/cmd/sbc/main.go new file mode 100644 index 0000000..4bf18bd --- /dev/null +++ b/cmd/sbc/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strconv" + "strings" + + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p" +) + +func main() { + bc := blockchain.NewBlockchain() + cfg := p2p.DefaultConfig() + cfg.ParseFlags() + + server, err := p2p.NewServer(cfg, bc) + if err != nil { + log.Fatalf("failed to create server: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + if err := server.Start(ctx); err != nil { + log.Printf("Server error: %v", err) + } + }() + + for _, addr := range cfg.BootNode { + if addr == "" { + continue + } + if err := server.ConnectToPeer(addr); err != nil { + log.Printf("Can't connect to peer %s: %v", addr, err) + } + } + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("Simple Blockchain CLI (P2P Enabled)") + fmt.Printf("Node ID: %s\n", server.GetHostID()) + fmt.Printf("Listening on: %s\n", server.GetAddrs()) + fmt.Println("Commands: add , print, validate, length, peers, addr, connect , sync, discover, find , help, quit") + + for { + fmt.Print("> ") + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + if input == "" { + continue + } + parts := strings.Fields(input) + command := parts[0] + args := parts[1:] + switch command { + case "add": + if len(parts) < 2 { + fmt.Println("Usage: add ") + continue + } + bpm, err := strconv.Atoi(parts[1]) + if err != nil { + fmt.Println("Error: BPM must be a number") + continue + } + bc.AddBlock(bpm) + fmt.Printf("Block added and broadcasting...\n") + case "print": + bc.Print() + case "validate": + if bc.IsValid() { + fmt.Println("Blockchain is valid!") + } else { + fmt.Println("Blockchain is INVALID!") + } + case "length": + fmt.Printf("Blockchain length: %d\n", bc.Length()) + case "peers": + peers := server.GetPeers() + if len(peers) == 0 { + fmt.Println("No peers connected") + continue + } + fmt.Printf("Connected peers (%d):\n", len(peers)) + for id, p := range peers { + fmt.Printf(" - %s (height: %d)\n", id, p.BestHeight) + } + case "addr": + addrs := server.GetAddrs() + nodeID := server.GetHostID() + fmt.Println("Your full addresses (share these to connect):") + for _, addr := range addrs { + fmt.Printf(" %s/p2p/%s\n", addr, nodeID) + } + case "connect": + if len(args) < 1 { + fmt.Println("Usage: connect ") + fmt.Println("Example: connect /ip4/127.0.0.1/tcp/8333/p2p/Qm...") + continue + } + addr := args[0] + if err := server.ConnectToPeer(addr); err != nil { + fmt.Printf("Error connecting to peer: %v\n", err) + } else { + fmt.Printf("Connected to %s\n", addr) + } + case "sync": + peers := server.GetPeers() + if len(peers) == 0 { + fmt.Println("No peers connected to sync") + continue + } + fmt.Println("Requesting blocks from peers...") + server.RequestSync() + case "discover": + peers := server.GetPeers() + if len(peers) == 0 { + fmt.Println("No peers connected to discover more") + continue + } + fmt.Println("Discovering more peers...") + server.DiscoverPeers() + case "find": + if len(args) < 1 { + fmt.Println("Usage: find ") + continue + } + hash := args[0] + peers := server.GetPeers() + if len(peers) == 0 { + fmt.Println("No peers connected to search") + continue + } + fmt.Printf("Searching for block: %s\n", hash) + server.FindBlock(hash) + case "help": + fmt.Println("Available commands:") + fmt.Println(" add - Add a new block with given BPM") + fmt.Println(" print - Print all blocks in the blockchain") + fmt.Println(" validate - Validate blockchain integrity") + fmt.Println(" length - Show blockchain length") + fmt.Println(" peers - Show connected peers") + fmt.Println(" addr - Show your full addresses to share") + fmt.Println(" connect - Connect to a peer via multiaddr (use 'addr' on other node)") + fmt.Println(" sync - Request blocks from connected peers") + fmt.Println(" discover - Discover more peers from connected peers") + fmt.Println(" find - Find a block by hash in the network") + fmt.Println(" help - Show this help message") + fmt.Println(" quit - Exit the program") + case "quit", "exit": + fmt.Println("Goodbye!") + return + default: + fmt.Printf("Unknown command: %s\n", command) + fmt.Println("Available commands: add, print, validate, length, peers, addr, connect, sync, discover, find, help, quit") + } + } +} diff --git a/cmd/sbcd/main.go b/cmd/sbcd/main.go new file mode 100644 index 0000000..cbad411 --- /dev/null +++ b/cmd/sbcd/main.go @@ -0,0 +1,256 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p" + "KitsuneSemCalda/SBC/internal/storage" + + "github.com/libp2p/go-libp2p/core/peer" +) + +var logger *slog.Logger + +const ( + ValidationInterval = 30 * time.Second + SyncInterval = 60 * time.Second + StatsInterval = 10 * time.Second +) + +type DaemonCallbacks struct { + blockchain *blockchain.Blockchain + server *p2p.Server +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func (c *DaemonCallbacks) OnNewPeer(id peer.ID) { + logger.Info("new peer connected", "peer", id.String()[:min(16, len(id.String()))]) +} + +func (c *DaemonCallbacks) OnDisconnect(id peer.ID) { + logger.Warn("peer disconnected", "peer", id.String()[:min(16, len(id.String()))]) +} + +func (c *DaemonCallbacks) OnPeerFound(info peer.AddrInfo) { + logger.Debug("peer found via discovery", "peer", info.ID.String()[:min(16, len(info.ID.String()))]) +} + +func (c *DaemonCallbacks) OnBlockReceived(block *blockchain.Block) { + hash := block.Hash + if len(hash) > 8 { + hash = hash[:8] + } + logger.Info("block received", "index", block.Index, "hash", hash) + logger.Debug("blockchain status", "length", c.blockchain.Length()) +} + +func initLogger() { + opts := &slog.HandlerOptions{ + Level: slog.LevelDebug, + } + // Use colored output for terminal + handler := &ColoredHandler{ + Handler: slog.NewTextHandler(os.Stdout, opts), + } + logger = slog.New(handler) +} + +type ColoredHandler struct { + slog.Handler +} + +func (h *ColoredHandler) Handle(ctx context.Context, r slog.Record) error { + level := r.Level.String() + var colorCode string + switch r.Level { + case slog.LevelDebug: + colorCode = "\033[36m" // Cyan + case slog.LevelInfo: + colorCode = "\033[32m" // Green + case slog.LevelWarn: + colorCode = "\033[33m" // Yellow + case slog.LevelError: + colorCode = "\033[31m" // Red + } + + fmt.Printf("%s[%s]\033[0m %s ", colorCode, level, r.Message) + r.Attrs(func(a slog.Attr) bool { + fmt.Printf("\033[90m%s=%v\033[0m ", a.Key, a.Value) + return true + }) + fmt.Println() + return nil +} + +func startMaintenance(ctx context.Context, bc *blockchain.Blockchain, server *p2p.Server) { + logger.Info("starting maintenance loops") + go validationTask(ctx, bc) + go syncTask(ctx, bc, server) + go statsTask(ctx, bc, server) +} + +func validationTask(ctx context.Context, bc *blockchain.Blockchain) { + ticker := time.NewTicker(ValidationInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if bc.IsValid() { + logger.Debug("✓ chain validation: OK", "height", bc.Length()) + } else { + logger.Error("✗ CRITICAL: chain is corrupted!") + } + } + } +} + +func syncTask(ctx context.Context, bc *blockchain.Blockchain, server *p2p.Server) { + ticker := time.NewTicker(SyncInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + peers := server.GetPeers() + if len(peers) == 0 { + continue + } + + var maxHeight int + var bestPeer peer.ID + for pID, p := range peers { + if p.BestHeight > maxHeight { + maxHeight = p.BestHeight + bestPeer = pID + } + } + + if maxHeight > bc.Length() { + logger.Info("sync: peer has longer chain", + "peer", bestPeer.String()[:8], + "peer_height", maxHeight, + "local_height", bc.Length()) + server.RequestSync() + } + } + } +} + +func statsTask(ctx context.Context, bc *blockchain.Blockchain, server *p2p.Server) { + ticker := time.NewTicker(StatsInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + peers := server.GetPeers() + lastBlock := bc.GetLastBlock() + logger.Info("stats update", + "height", bc.Length(), + "peers", len(peers), + "last_hash", lastBlock.Hash[:8]) + } + } +} + +func main() { + initLogger() + logger.Info("starting sbc daemon") + + bc := blockchain.NewBlockchain() + cfg := p2p.DefaultConfig() + // Daemon should default to 8333 + cfg.ListenAddr = "/ip4/0.0.0.0/tcp/8333" + cfg.ParseFlags() + + store, err := storage.NewStore(cfg.DataDir) + if err != nil { + logger.Error("failed to open database", "error", err) + os.Exit(1) + } + defer store.Close() + + err = store.Load(bc) + if err != nil { + logger.Error("failed to load blockchain from store", "error", err) + os.Exit(1) + } + + server, err := p2p.NewServer(cfg, bc) + if err != nil { + logger.Error("failed to create server", "error", err) + os.Exit(1) + } + + cbs := &DaemonCallbacks{blockchain: bc, server: server} + server.SetPeerCallback(cbs) + server.SetBlockCallback(cbs) + + for _, addr := range cfg.BootNode { + if addr == "" { + continue + } + err = server.ConnectToPeer(addr) + if err != nil { + logger.Warn("can't connect to bootnode", "address", addr, "error", err) + } + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + startMaintenance(ctx, bc, server) + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-stop + err := store.Save(bc) + if err != nil { + logger.Error("failed to save blockchain", "error", err) + } else { + logger.Info("blockchain saved successfully") + } + cancel() + }() + + logger.Info("daemon initialized", + "peer_id", server.GetHostID(), + "listening", server.GetAddrs(), + "height", bc.Length()) + + announceData := map[string]string{ + "peer_id": server.GetHostID(), + "addr": "/ip4/0.0.0.0/tcp/8333", + } + announceBytes, _ := json.Marshal(announceData) + os.WriteFile("/tmp/sbc-daemon.json", announceBytes, 0644) + logger.Info("announce file written", "path", "/tmp/sbc-daemon.json") + + err = server.Start(ctx) + if err != nil { + logger.Error("server error", "error", err) + } +} diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..bb445f2 --- /dev/null +++ b/cmd/seed/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p" + "KitsuneSemCalda/SBC/internal/storage" +) + +type SeedServer struct { + peers map[string]time.Time + mu sync.Mutex +} + +type SeedPeerInfo struct { + Addr string `json:"addr"` + SeenAt int64 `json:"seen_at"` +} + +type SeedResponse struct { + Peers []SeedPeerInfo `json:"peers"` +} + +func main() { + bc := blockchain.NewBlockchain() + cfg := p2p.DefaultConfig() + cfg.ParseFlags() + + store, err := storage.NewStore(cfg.DataDir) + if err != nil { + log.Fatalf("failed to open database: %v", err) + } + defer store.Close() + + err = store.Load(bc) + if err != nil { + log.Fatalf("failed to load blockchain from store: %v", err) + } + + server, err := p2p.NewServer(cfg, bc) + if err != nil { + log.Fatalf("failed to create server: %v", err) + } + + seed := &SeedServer{ + peers: make(map[string]time.Time), + } + + go func() { + for { + time.Sleep(30 * time.Second) + seed.cleanupPeers() + } + }() + + go func() { + for { + time.Sleep(10 * time.Second) + seed.updatePeers(server) + } + }() + + http.HandleFunc("/seeds", seed.handleSeeds) + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }) + + addr := ":8080" + if env := os.Getenv("SEED_PORT"); env != "" { + addr = ":" + env + } + + log.Printf("Seed server starting on http://localhost%s", addr) + log.Printf("Seed endpoint: http://localhost%s/seeds", addr) + + go func() { + if err := http.ListenAndServe(addr, nil); err != nil && err != http.ErrServerClosed { + log.Fatalf("seed server error: %v", err) + } + }() + + for _, bootAddr := range cfg.BootNode { + if bootAddr == "" { + continue + } + err = server.ConnectToPeer(bootAddr) + if err != nil { + log.Printf("Can't connect to bootnode %s: %v", bootAddr, err) + } + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-stop + err := store.Save(bc) + if err != nil { + log.Printf("failed to save blockchain: %v", err) + } else { + log.Println("blockchain saved successfully") + } + cancel() + }() + + log.Printf("Daemon Node ID: %s", server.GetHostID()) + log.Printf("Listening on: %s", server.GetAddrs()) + + server.Start(ctx) +} + +func (s *SeedServer) updatePeers(server *p2p.Server) { + peers := server.GetPeers() + s.mu.Lock() + defer s.mu.Unlock() + + for id := range peers { + for _, addr := range server.GetAddrs() { + peerAddr := addr.String() + "/p2p/" + id.String() + s.peers[peerAddr] = time.Now() + } + } +} + +func (s *SeedServer) cleanupPeers() { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now() + for addr, seen := range s.peers { + if now.Sub(seen) > 5*time.Minute { + delete(s.peers, addr) + } + } +} + +func (s *SeedServer) handleSeeds(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + defer s.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + + var peers []SeedPeerInfo + for addr, seen := range s.peers { + peers = append(peers, SeedPeerInfo{ + Addr: addr, + SeenAt: seen.Unix(), + }) + } + + if len(peers) == 0 { + log.Printf("[SEED] No peers available") + } else { + log.Printf("[SEED] Serving %d peers", len(peers)) + } + + json.NewEncoder(w).Encode(SeedResponse{Peers: peers}) +} diff --git a/cmd/tester/main.go b/cmd/tester/main.go new file mode 100644 index 0000000..a76c918 --- /dev/null +++ b/cmd/tester/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "flag" + "os" + "strings" + + _ "KitsuneSemCalda/SBC/Tests/background" + _ "KitsuneSemCalda/SBC/Tests/e2e" + _ "KitsuneSemCalda/SBC/Tests/unit/blockchain" + _ "KitsuneSemCalda/SBC/Tests/unit/p2p" + _ "KitsuneSemCalda/SBC/Tests/unit/sbc" + _ "KitsuneSemCalda/SBC/Tests/unit/sbcd" + _ "KitsuneSemCalda/SBC/Tests/unit/seed" + _ "KitsuneSemCalda/SBC/Tests/unit/storage" + "github.com/caiolandgraf/gest/gest" +) + +func main() { + all := flag.Bool("all", true, "Run all tests") + unit := flag.Bool("unit", false, "Run unit tests") + e2e := flag.Bool("e2e", false, "Run e2e tests") + background := flag.Bool("background", false, "Run background tests") + + flag.Parse() + + // If any specific flag is set, don't run all by default unless --all is explicit + anySpecific := *unit || *e2e || *background + if anySpecific && !isFlagPassed("all") { + *all = false + } + + filter := "" + if !*all { + var filters []string + if *unit { + filters = append(filters, "Block", "Blockchain", "BlockchainNode", "P2P Config", "P2P Message", "P2P Peer", "Storage Store", "SBC CLI Logic", "SBCD Daemon Logic", "Seed Server Logic") + } + if *e2e { + filters = append(filters, "Blockchain E2E", "P2P E2E", "Storage E2E") + } + if *background { + filters = append(filters, "Blockchain Background", "P2P Background", "Storage Background") + } + filter = strings.Join(filters, "|") + } + + if filter != "" { + // gest.RunRegistered handles filtering via regex if supported + } + + if !gest.RunRegistered() { + os.Exit(1) + } +} + +func isFlagPassed(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d3bce6b --- /dev/null +++ b/go.mod @@ -0,0 +1,122 @@ +module KitsuneSemCalda/SBC + +go 1.25.7 + +require ( + github.com/caiolandgraf/gest v1.0.0 + github.com/libp2p/go-libp2p v0.47.0 + github.com/libp2p/go-libp2p-kad-dht v0.38.0 + github.com/mattn/go-sqlite3 v1.14.34 + github.com/multiformats/go-multiaddr v0.16.1 +) + +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/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/boxo v0.36.0 // indirect + github.com/ipfs/go-cid v0.6.0 // indirect + github.com/ipfs/go-datastore v0.9.1 // indirect + github.com/ipfs/go-log/v2 v2.9.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/cpuid/v2 v2.3.0 // indirect + github.com/koron/go-ssdp v0.0.6 // 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.3.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-libp2p-kbucket v0.8.0 // 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.4.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect + github.com/libp2p/zeroconf/v2 v2.2.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.72 // 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.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.1.1 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // 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.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v3 v3.0.6 // 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/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/polydawn/refmt v0.89.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.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.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.5.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.41.0 // indirect + gonum.org/v1/gonum v0.17.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c28080b --- /dev/null +++ b/go.sum @@ -0,0 +1,377 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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 v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caiolandgraf/gest v1.0.0 h1:EodZa++HCjJQet3ZhuLAr8ZHKkZkK/2RnxmHzlvHxj8= +github.com/caiolandgraf/gest v1.0.0/go.mod h1:sYUNhjQdSOwEotneNKnUkeu7GQene6YtwLjB0OOtrPk= +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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/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/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/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.36.0 h1:DarrMBM46xCs6GU6Vz+AL8VUyXykqHAqZYx8mR0Oics= +github.com/ipfs/boxo v0.36.0/go.mod h1:92hnRXfP5ScKEIqlq9Ns7LR1dFXEVADKWVGH0fjk83k= +github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= +github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= +github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= +github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= +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-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk= +github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= +github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= +github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= +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/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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +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/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/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.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= +github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= +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.38.0 h1:NToFzwvICo6ghDfSwuTmROCtl9LDXSZT1VawEbm4NUs= +github.com/libp2p/go-libp2p-kad-dht v0.38.0/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= +github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= +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.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +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.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= +github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +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/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.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/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.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +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/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.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +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.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +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/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +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 v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +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.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +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/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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-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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/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-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-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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.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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +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= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/internal/blockchain/block.go b/internal/blockchain/block.go new file mode 100644 index 0000000..b0ac798 --- /dev/null +++ b/internal/blockchain/block.go @@ -0,0 +1,84 @@ +package blockchain + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "strconv" + "time" +) + +const ( + MaxTimeDrift = 10 * time.Minute + MaxBlockAge = 24 * time.Hour +) + +type Block struct { + Index int + Timestamp time.Time + BPM int + Hash string + PrevHash string +} + +func (b *Block) CalculateHash() string { + data := strconv.Itoa(b.Index) + b.Timestamp.Format(time.RFC3339) + strconv.Itoa(b.BPM) + b.PrevHash + hash := sha256.Sum256([]byte(data)) + + return hex.EncodeToString(hash[:]) +} + +func (b *Block) Validate(prevBlock *Block) error { + // Verificar hash + if b.Hash != b.CalculateHash() { + return errors.New("invalid hash") + } + + // Verificar encadeamento + if prevBlock != nil { + if b.Index != prevBlock.Index+1 { + return errors.New("invalid index") + } + if b.PrevHash != prevBlock.Hash { + return errors.New("invalid prev hash") + } + } else if b.Index != 0 { + return errors.New("missing previous block for non-genesis block") + } + + // Verificar timestamp + now := time.Now() + if b.Timestamp.After(now.Add(MaxTimeDrift)) { + return errors.New("timestamp too far in future") + } + + // Verificar BPM + if b.BPM < 0 { + return errors.New("invalid BPM") + } + + return nil +} + +func NewBlock(index int, BPM int, prevHash string) *Block { + block := &Block{ + Index: index, + Timestamp: time.Now(), + BPM: BPM, + PrevHash: prevHash, + } + + block.Hash = block.CalculateHash() + return block +} + +func GenerateGenesisBlock() *Block { + block := &Block{ + Index: 0, + Timestamp: time.Unix(0, 0).UTC(), + BPM: 0, + PrevHash: "0", + } + block.Hash = block.CalculateHash() + return block +} diff --git a/internal/blockchain/blockchain.go b/internal/blockchain/blockchain.go new file mode 100644 index 0000000..4296f35 --- /dev/null +++ b/internal/blockchain/blockchain.go @@ -0,0 +1,242 @@ +package blockchain + +import ( + "fmt" + "sync" +) + +type BlockCallback func(*Block) + +type Blockchain struct { + head *BlockchainNode + tail *BlockchainNode + length int + callbacks []BlockCallback + mu sync.RWMutex +} + +func NewBlockchain() *Blockchain { + genesisBlock := GenerateGenesisBlock() + genesisNode := NewBlockchainNode(genesisBlock) + + return &Blockchain{ + head: genesisNode, + tail: genesisNode, + length: 1, + callbacks: []BlockCallback{}, + } +} + +func (bc *Blockchain) AddBlock(bpm int) { + bc.mu.Lock() + defer bc.mu.Unlock() + newBlock := NewBlock(bc.tail.m_block.Index+1, bpm, bc.tail.m_block.Hash) + bc.processBlockInternal(newBlock) +} + +func (bc *Blockchain) ProcessBlock(block *Block) { + bc.mu.Lock() + defer bc.mu.Unlock() + bc.processBlockInternal(block) +} + +func (bc *Blockchain) processBlockInternal(block *Block) { + if err := block.Validate(bc.tail.m_block); err != nil { + return + } + + newNode := NewBlockchainNode(block) + newNode.prev_block = bc.tail + bc.tail.next_block = newNode + bc.tail = newNode + bc.length++ + + for _, cb := range bc.callbacks { + cb(block) + } +} + +func (bc *Blockchain) Subscribe(cb BlockCallback) { + bc.mu.Lock() + defer bc.mu.Unlock() + bc.callbacks = append(bc.callbacks, cb) +} + +func (bc *Blockchain) GetLastBlock() *Block { + bc.mu.RLock() + defer bc.mu.RUnlock() + return bc.tail.m_block +} + +func (bc *Blockchain) IsValid() bool { + bc.mu.RLock() + defer bc.mu.RUnlock() + current := bc.head + + for current != nil { + var prev *Block + if current.prev_block != nil { + prev = current.prev_block.m_block + } + if err := current.m_block.Validate(prev); err != nil { + return false + } + + current = current.next_block + } + + return true +} + +func (bc *Blockchain) Print() { + bc.mu.RLock() + defer bc.mu.RUnlock() + current := bc.head + for current != nil { + fmt.Printf("Index: %d, BPM: %d, Hash: %s\n", + current.m_block.Index, + current.m_block.BPM, + current.m_block.Hash) + current = current.next_block + } +} + +func (bc *Blockchain) Length() int { + bc.mu.RLock() + defer bc.mu.RUnlock() + return bc.length +} + +func (bc *Blockchain) GetAllBlocks() []*Block { + bc.mu.RLock() + defer bc.mu.RUnlock() + var blocks []*Block + current := bc.head + for current != nil { + blocks = append(blocks, current.m_block) + current = current.next_block + } + return blocks +} + +func (bc *Blockchain) GetBlockByHash(hash string) *Block { + bc.mu.RLock() + defer bc.mu.RUnlock() + current := bc.head + for current != nil { + if current.m_block.Hash == hash { + return current.m_block + } + current = current.next_block + } + return nil +} + +func (bc *Blockchain) GetBlocksAfter(hash string, limit int) []*Block { + bc.mu.RLock() + defer bc.mu.RUnlock() + var blocks []*Block + current := bc.head + + if hash != "" { + for current != nil && current.m_block.Hash != hash { + current = current.next_block + } + + if current != nil { + current = current.next_block + } else { + current = bc.head + } + } + + count := 0 + for current != nil && count < limit { + blocks = append(blocks, current.m_block) + current = current.next_block + count++ + } + + return blocks +} + +func (bc *Blockchain) ValidateChain(blocks []*Block) (bool, string) { + // This doesn't need a lock because it only depends on input + if len(blocks) == 0 { + return false, "empty chain" + } + + genesis := GenerateGenesisBlock() + if blocks[0].Index == 0 { + if blocks[0].Hash != genesis.Hash { + return false, "genesis block mismatch" + } + } + + if err := blocks[0].Validate(nil); err != nil { + return false, "first block invalid: " + err.Error() + } + + for i := 1; i < len(blocks); i++ { + if err := blocks[i].Validate(blocks[i-1]); err != nil { + return false, fmt.Sprintf("block %d invalid: %v", i, err) + } + } + + return true, "" +} + +func (bc *Blockchain) ReplaceChain(blocks []*Block) bool { + bc.mu.Lock() + defer bc.mu.Unlock() + + if len(blocks) <= bc.length { + return false + } + + valid, _ := bc.ValidateChain(blocks) + if !valid { + return false + } + + bc.head = nil + bc.tail = nil + bc.length = 0 + + for _, block := range blocks { + newNode := NewBlockchainNode(block) + if bc.head == nil { + bc.head = newNode + bc.tail = newNode + } else { + newNode.prev_block = bc.tail + bc.tail.next_block = newNode + bc.tail = newNode + } + bc.length++ + } + + return true +} + +func (bc *Blockchain) TryAcceptChain(newBlocks []*Block) (bool, string) { + // We check length before locking for performance, then check again inside ReplaceChain + bc.mu.RLock() + localLen := bc.length + bc.mu.RUnlock() + + if len(newBlocks) <= localLen { + return false, "chain not longer" + } + + valid, reason := bc.ValidateChain(newBlocks) + if !valid { + return false, reason + } + + if bc.ReplaceChain(newBlocks) { + return true, "chain replaced with longer version" + } + + return false, "failed to replace chain" +} diff --git a/internal/blockchain/blockchain_node.go b/internal/blockchain/blockchain_node.go new file mode 100644 index 0000000..4e18b68 --- /dev/null +++ b/internal/blockchain/blockchain_node.go @@ -0,0 +1,23 @@ +package blockchain + +import "fmt" + +type BlockchainNode struct { + m_block *Block + next_block *BlockchainNode + prev_block *BlockchainNode +} + +func NewBlockchainNode(block *Block) *BlockchainNode { + return &BlockchainNode{ + block, + nil, + nil, + } +} + +func (bn *BlockchainNode) String() string { + return fmt.Sprintf("Block #%d [Hash: %s...]", + bn.m_block.Index, + bn.m_block.Hash[:min(8, len(bn.m_block.Hash))]) +} diff --git a/internal/blockchain/blockchain_spec.go b/internal/blockchain/blockchain_spec.go new file mode 100644 index 0000000..86f4c54 --- /dev/null +++ b/internal/blockchain/blockchain_spec.go @@ -0,0 +1,33 @@ +package blockchain + +import ( + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("Blockchain") + + s.It("should create a blockchain with a genesis block", func(t *gest.T) { + bc := NewBlockchain() + t.Expect(bc.Length()).ToBe(1) + t.Expect(bc.GetLastBlock()).Not().ToBeNil() + t.Expect(bc.GetLastBlock().Index).ToBe(0) + }) + + s.It("should add blocks correctly", func(t *gest.T) { + bc := NewBlockchain() + bc.AddBlock(100) + t.Expect(bc.Length()).ToBe(2) + t.Expect(bc.GetLastBlock().BPM).ToBe(100) + t.Expect(bc.GetLastBlock().Index).ToBe(1) + }) + + s.It("should validate the blockchain correctly", func(t *gest.T) { + bc := NewBlockchain() + bc.AddBlock(100) + bc.AddBlock(200) + t.Expect(bc.IsValid()).ToBeTrue() + }) + + gest.Register(s) +} diff --git a/internal/p2p/callbacks/block_callbacks.go b/internal/p2p/callbacks/block_callbacks.go new file mode 100644 index 0000000..4653836 --- /dev/null +++ b/internal/p2p/callbacks/block_callbacks.go @@ -0,0 +1,7 @@ +package callbacks + +import "KitsuneSemCalda/SBC/internal/blockchain" + +type BlockCallbacks interface { + OnBlockReceived(block *blockchain.Block) +} diff --git a/internal/p2p/callbacks/peer_callbacks.go b/internal/p2p/callbacks/peer_callbacks.go new file mode 100644 index 0000000..29d65a1 --- /dev/null +++ b/internal/p2p/callbacks/peer_callbacks.go @@ -0,0 +1,9 @@ +package callbacks + +import "github.com/libp2p/go-libp2p/core/peer" + +type PeerCallbacks interface { + OnNewPeer(id peer.ID) + OnDisconnect(id peer.ID) + OnPeerFound(info peer.AddrInfo) +} diff --git a/internal/p2p/config.go b/internal/p2p/config.go new file mode 100644 index 0000000..e9918dd --- /dev/null +++ b/internal/p2p/config.go @@ -0,0 +1,123 @@ +package p2p + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/multiformats/go-multiaddr" +) + +type Config struct { + ListenAddr string + DataDir string + BootNode []string + DNSSeed string + EnableMDNS bool + EnableUDP bool + DiscoveryPort int + EnableDHT bool + Rendezvous string + BootstrapPeers []string +} + +func DefaultDataDir() string { + if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { + return filepath.Join(xdg, "sbc") + } + + if home := os.Getenv("HOME"); home != "" { + return filepath.Join(home, ".local/share/sbc") + } + + if home := os.Getenv("USERPROFILE"); home != "" { + return filepath.Join(home, "AppData", "Roaming", "SBC") + } + + return "sbc_data" +} + +var DefaultBootNodes = []string{} + +var WellKnownBootstrapNodes = []string{} + +var DefaultDNSServers = []string{} + +func GetBootstrapNodes() []string { + if env := os.Getenv("SBC_BOOTNODES"); env != "" { + return strings.Split(env, ",") + } + if len(WellKnownBootstrapNodes) > 0 { + return WellKnownBootstrapNodes + } + return DefaultBootNodes +} + +func GetDNSServers() string { + if env := os.Getenv("SBC_DNS"); env != "" { + return env + } + if len(DefaultDNSServers) > 0 { + return strings.Join(DefaultDNSServers, ",") + } + return "" +} + +func DefaultConfig() *Config { + bootNodes := GetBootstrapNodes() + + return &Config{ + ListenAddr: "/ip4/0.0.0.0/tcp/0", // 0 = random port + DataDir: DefaultDataDir(), + BootNode: bootNodes, + DNSSeed: GetDNSServers(), + EnableMDNS: true, + EnableUDP: true, + DiscoveryPort: 9999, + EnableDHT: true, + Rendezvous: "sbc-peers", + } +} + +func (c *Config) ParseFlags() { + var bootNodes string + var bootstrapPeers string + flag.StringVar(&c.ListenAddr, "listen", c.ListenAddr, "Address to listen on") + flag.StringVar(&c.DataDir, "datadir", c.DataDir, "Directory to store blockchain data") + flag.StringVar(&bootNodes, "bootnode", "", "Comma-separated list of boot nodes") + flag.StringVar(&c.DNSSeed, "dns", c.DNSSeed, "DNS seed servers for peer discovery") + flag.BoolVar(&c.EnableMDNS, "enable-mdns", c.EnableMDNS, "Enable mDNS discovery") + flag.BoolVar(&c.EnableUDP, "enable-udp", c.EnableUDP, "Enable UDP broadcast discovery") + flag.IntVar(&c.DiscoveryPort, "discovery-port", c.DiscoveryPort, "UDP port for discovery broadcast") + flag.BoolVar(&c.EnableDHT, "enable-dht", c.EnableDHT, "Enable Kademlia DHT discovery") + flag.StringVar(&c.Rendezvous, "rendezvous", c.Rendezvous, "Rendezvous string for DHT discovery") + flag.StringVar(&bootstrapPeers, "bootstrap", "", "Comma-separated list of DHT bootstrap peers") + flag.Parse() + + if bootNodes != "" { + c.BootNode = strings.Split(bootNodes, ",") + } + if bootstrapPeers != "" { + c.BootstrapPeers = strings.Split(bootstrapPeers, ",") + } +} + +func (c *Config) GetBootNodesAddr() ([]multiaddr.Multiaddr, error) { + var addrs []multiaddr.Multiaddr + + for _, addr := range c.BootNode { + if addr == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + return nil, fmt.Errorf("invalid multiaddr %s: %w", addr, err) + } + + addrs = append(addrs, ma) + } + + return addrs, nil +} diff --git a/internal/p2p/config_test.go b/internal/p2p/config_test.go new file mode 100644 index 0000000..a9a44e6 --- /dev/null +++ b/internal/p2p/config_test.go @@ -0,0 +1,104 @@ +package p2p + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDefaultDataDir(t *testing.T) { + // Original environment values to restore after test + origXdg := os.Getenv("XDG_DATA_HOME") + origHome := os.Getenv("HOME") + origUserProfile := os.Getenv("USERPROFILE") + + defer func() { + os.Setenv("XDG_DATA_HOME", origXdg) + os.Setenv("HOME", origHome) + os.Setenv("USERPROFILE", origUserProfile) + }() + + tests := []struct { + name string + env map[string]string + expected string + }{ + { + name: "XDG_DATA_HOME set", + env: map[string]string{ + "XDG_DATA_HOME": "/tmp/xdg", + "HOME": "/tmp/home", + "USERPROFILE": "/tmp/userprofile", + }, + expected: filepath.Join("/tmp/xdg", "sbc"), + }, + { + name: "HOME set, XDG_DATA_HOME empty", + env: map[string]string{ + "XDG_DATA_HOME": "", + "HOME": "/tmp/home", + "USERPROFILE": "/tmp/userprofile", + }, + expected: filepath.Join("/tmp/home", ".local/share/sbc"), + }, + { + name: "USERPROFILE set, XDG_DATA_HOME and HOME empty", + env: map[string]string{ + "XDG_DATA_HOME": "", + "HOME": "", + "USERPROFILE": `C:\Users\Test`, + }, + expected: filepath.Join(`C:\Users\Test`, "AppData", "Roaming", "SBC"), + }, + { + name: "All empty", + env: map[string]string{ + "XDG_DATA_HOME": "", + "HOME": "", + "USERPROFILE": "", + }, + expected: "sbc_data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear env + os.Setenv("XDG_DATA_HOME", "") + os.Setenv("HOME", "") + os.Setenv("USERPROFILE", "") + + // Set test env + for k, v := range tt.env { + os.Setenv(k, v) + } + + got := DefaultDataDir() + if got != tt.expected { + t.Errorf("DefaultDataDir() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestDefaultConfig(t *testing.T) { + origEnv := os.Getenv("SBC_BOOTNODES") + defer os.Setenv("SBC_BOOTNODES", origEnv) + + t.Run("Default boot nodes", func(t *testing.T) { + os.Setenv("SBC_BOOTNODES", "") + cfg := DefaultConfig() + if len(cfg.BootNode) != len(DefaultBootNodes) { + t.Errorf("expected %d boot nodes, got %d", len(DefaultBootNodes), len(cfg.BootNode)) + } + }) + + t.Run("Environment override", func(t *testing.T) { + customNode := "/ip4/1.2.3.4/tcp/1234/p2p/QmTest" + os.Setenv("SBC_BOOTNODES", customNode) + cfg := DefaultConfig() + if len(cfg.BootNode) != 1 || cfg.BootNode[0] != customNode { + t.Errorf("expected custom boot node %s, got %v", customNode, cfg.BootNode) + } + }) +} diff --git a/internal/p2p/host.go b/internal/p2p/host.go new file mode 100644 index 0000000..b4ee3a2 --- /dev/null +++ b/internal/p2p/host.go @@ -0,0 +1,307 @@ +package p2p + +import ( + "context" + "encoding/json" + "fmt" + "net" + "sync" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/discovery/mdns" + "github.com/libp2p/go-libp2p/p2p/discovery/routing" + "github.com/libp2p/go-libp2p/p2p/discovery/util" + "github.com/libp2p/go-libp2p/p2p/protocol/ping" + "github.com/multiformats/go-multiaddr" +) + +const ( + ProtocolID = "/sbc/1.0.0" +) + +type HostCallbacks interface { + OnPeerFound(peer.AddrInfo) +} + +type Host struct { + host host.Host + ping *ping.PingService + config *Config + + callbacks HostCallbacks + cbMutex sync.RWMutex + mdns mdns.Service + dht *dht.IpfsDHT +} + +// HandlePeerFound implements the mdns.Notifee interface +func (h *Host) HandlePeerFound(pi peer.AddrInfo) { + h.cbMutex.RLock() + cb := h.callbacks + h.cbMutex.RUnlock() + + if cb != nil { + cb.OnPeerFound(pi) + } +} + +type DiscoveryMsg struct { + Type string `json:"type"` + PeerID string `json:"peer_id"` + Addresses []string `json:"addresses"` +} + +func startUDPBroadcast(cfg *Config, h *Host, getAddrs func() []multiaddr.Multiaddr, getID func() peer.ID) { + if !cfg.EnableUDP { + return + } + + addr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("255.255.255.255:%d", cfg.DiscoveryPort)) + if err != nil { + fmt.Printf("[UDP] Error: failed to resolve broadcast address: %v\n", err) + return + } + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: cfg.DiscoveryPort}) + if err != nil { + fmt.Printf("[UDP] Warning: cannot listen on UDP %d: %v. Discovery only mode.\n", cfg.DiscoveryPort, err) + return + } + + fmt.Printf("[UDP] Discovery broadcast listening on :%d\n", cfg.DiscoveryPort) + + go func() { + defer conn.Close() + buf := make([]byte, 1024) + for { + n, remoteAddr, err := conn.ReadFromUDP(buf) + if err != nil { + continue + } + + var msg DiscoveryMsg + if err := json.Unmarshal(buf[:n], &msg); err != nil { + continue + } + + if msg.Type == "announce" && msg.PeerID != "" { + peerID, err := peer.Decode(msg.PeerID) + if err != nil { + continue + } + + if peerID == getID() { + continue + } + + var addrs []multiaddr.Multiaddr + for _, a := range msg.Addresses { + ma, err := multiaddr.NewMultiaddr(a) + if err == nil { + addrs = append(addrs, ma) + } + } + + if len(addrs) > 0 { + pi := peer.AddrInfo{ + ID: peerID, + Addrs: addrs, + } + fmt.Printf("[UDP] Discovered peer: %s from %s\n", peerID, remoteAddr.IP) + h.HandlePeerFound(pi) + } + } + } + }() + + go func() { + for { + peerID := getID() + addrs := getAddrs() + if peerID != "" && len(addrs) > 0 { + var addrStrings []string + for _, a := range addrs { + addrStrings = append(addrStrings, a.String()) + } + msg := DiscoveryMsg{ + Type: "announce", + PeerID: peerID.String(), + Addresses: addrStrings, + } + data, _ := json.Marshal(msg) + conn.WriteToUDP(data, addr) + } + time.Sleep(10 * time.Second) + } + }() +} + +func (h *Host) startDHT(ctx context.Context) error { + opts := []dht.Option{ + dht.Mode(dht.ModeAuto), + } + + kdht, err := dht.New(ctx, h.host, opts...) + if err != nil { + return err + } + + if err = kdht.Bootstrap(ctx); err != nil { + return err + } + + h.dht = kdht + + // Connect to bootstrap nodes + var wg sync.WaitGroup + bootstrapPeers := dht.DefaultBootstrapPeers + if len(h.config.BootstrapPeers) > 0 { + bootstrapPeers = []multiaddr.Multiaddr{} + for _, s := range h.config.BootstrapPeers { + ma, err := multiaddr.NewMultiaddr(s) + if err == nil { + bootstrapPeers = append(bootstrapPeers, ma) + } + } + } + + for _, peerAddr := range bootstrapPeers { + pi, err := peer.AddrInfoFromP2pAddr(peerAddr) + if err != nil { + continue + } + wg.Add(1) + go func(pi peer.AddrInfo) { + defer wg.Done() + if err := h.host.Connect(ctx, pi); err != nil { + // Silently fail for default bootstrap nodes to avoid log noise + } else { + fmt.Printf("[DHT] Connected to bootstrap node %s\n", pi.ID) + } + }(*pi) + } + + routingDiscovery := routing.NewRoutingDiscovery(kdht) + util.Advertise(ctx, routingDiscovery, h.config.Rendezvous) + + // Look for peers + go func() { + ticker := time.NewTicker(time.Second * 30) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + peers, err := routingDiscovery.FindPeers(ctx, h.config.Rendezvous) + if err != nil { + fmt.Printf("[DHT] Error finding peers: %v\n", err) + continue + } + + for p := range peers { + if p.ID == h.host.ID() { + continue + } + h.HandlePeerFound(p) + } + } + } + }() + + return nil +} + +func NewHost(cfg *Config, cb HostCallbacks) (*Host, error) { + h, err := libp2p.New( + libp2p.ListenAddrStrings(cfg.ListenAddr), + libp2p.DisableRelay(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create host: %w", err) + } + pingService := ping.NewPingService(h) + + host := &Host{ + host: h, + ping: pingService, + config: cfg, + callbacks: cb, + } + + if cfg.EnableMDNS { + // Use 'host' itself as the notifee + mdnsService := mdns.NewMdnsService(h, "sbc-p2p", host) + if mdnsService == nil { + fmt.Println("[mDNS] Warning: mDNS discovery not available") + } else { + if err := mdnsService.Start(); err != nil { + fmt.Printf("[mDNS] Error starting discovery service: %v\n", err) + } else { + host.mdns = mdnsService + fmt.Println("[mDNS] Discovery service started") + } + } + } + + if cfg.EnableUDP { + startUDPBroadcast(cfg, host, h.Addrs, h.ID) + } + + if cfg.EnableDHT { + if err := host.startDHT(context.Background()); err != nil { + fmt.Printf("[DHT] Error starting DHT: %v\n", err) + } else { + fmt.Println("[DHT] Discovery service started") + } + } + + return host, nil +} + +func (h *Host) SetPeerCallback(cb HostCallbacks) { + h.cbMutex.Lock() + defer h.cbMutex.Unlock() + h.callbacks = cb +} + +func (h *Host) ID() peer.ID { + return h.host.ID() +} + +func (h *Host) Addrs() []multiaddr.Multiaddr { + return h.host.Addrs() +} + +func (h *Host) Connect(ctx context.Context, addr multiaddr.Multiaddr) error { + pi, err := peer.AddrInfoFromP2pAddr(addr) + if err != nil { + return err + } + return h.host.Connect(ctx, *pi) +} + +func (h *Host) ConnectPeer(ctx context.Context, pi peer.AddrInfo) error { + return h.host.Connect(ctx, pi) +} + +func (h *Host) NewStream(ctx context.Context, p peer.ID) (network.Stream, error) { + return h.host.NewStream(ctx, p, ProtocolID) +} + +func (h *Host) SetStreamHandler(handler network.StreamHandler) { + h.host.SetStreamHandler(ProtocolID, handler) +} + +func (h *Host) Close() error { + if h.dht != nil { + h.dht.Close() + } + return h.host.Close() +} diff --git a/internal/p2p/message.go b/internal/p2p/message.go new file mode 100644 index 0000000..8f33a83 --- /dev/null +++ b/internal/p2p/message.go @@ -0,0 +1,100 @@ +package p2p + +import ( + "encoding/json" + "time" +) + +type MessageType string + +const ( + MsgVersion MessageType = "version" + MsgVerAck MessageType = "verack" + MsgGetBlocks MessageType = "getblocks" + MsgBlock MessageType = "block" + MsgInv MessageType = "inv" + MsgGetData MessageType = "getdata" + MsgPing MessageType = "ping" + MsgPong MessageType = "pong" + MsgGetPeers MessageType = "getpeers" + MsgPeers MessageType = "peers" + MsgFindBlock MessageType = "findblock" + MsgBlockFound MessageType = "blockfound" +) + +type Message struct { + Type MessageType `json:"type"` + Payload json.RawMessage `json:"payload,omitempty"` +} + +type VersionPayload struct { + Version int `json:"version"` + Timestamp time.Time `json:"timestamp"` + AddrRecv string `json:"addr_recv"` + AddrFrom string `json:"addr_from"` + UserAgent string `json:"user_agent"` + BestHeight int `json:"best_height"` +} + +type VerAckPayload struct { + Accept bool `json:"accept"` +} + +type BlockPayload struct { + Index int `json:"index"` + Timestamp time.Time `json:"timestamp"` + BPM int `json:"bpm"` + Hash string `json:"hash"` + PrevHash string `json:"prev_hash"` +} + +type GetBlocksPayload struct { + StartHash string `json:"start_hash"` + StopHash string `json:"stop_hash"` +} + +type InvPayload struct { + Count int `json:"count"` + InvVec []InvVec `json:"inv_vec"` +} + +type InvVec struct { + Type string `json:"type"` // "block" + Hash string `json:"hash"` +} + +type GetPeersPayload struct{} + +type PeersPayload struct { + Peers []string `json:"peers"` +} + +type FindBlockPayload struct { + Hash string `json:"hash"` +} + +type BlockFoundPayload struct { + Found bool `json:"found"` + Block *BlockPayload `json:"block,omitempty"` +} + +func NewMessage(msgType MessageType, payload any) (*Message, error) { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + return &Message{ + Type: msgType, + Payload: payloadBytes, + }, nil +} + +func (m *Message) Encode() ([]byte, error) { + return json.Marshal(m) +} + +func DecodeMessage(data []byte) (*Message, error) { + var msg Message + err := json.Unmarshal(data, &msg) + return &msg, err +} diff --git a/internal/p2p/p2p_spec.go b/internal/p2p/p2p_spec.go new file mode 100644 index 0000000..dd14379 --- /dev/null +++ b/internal/p2p/p2p_spec.go @@ -0,0 +1,27 @@ +package p2p + +import ( + "KitsuneSemCalda/SBC/internal/blockchain" + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("P2P Server") + + s.It("should create a P2P server with correct default config", func(t *gest.T) { + bc := blockchain.NewBlockchain() + cfg := DefaultConfig() + server, err := NewServer(cfg, bc) + + t.Expect(err).ToBeNil() + t.Expect(server).Not().ToBeNil() + t.Expect(server.GetHostID()).Not().ToBe("") + }) + + s.It("should have a valid protocol ID", func(t *gest.T) { + t.Expect(ProtocolID).Not().ToBe("") + t.Expect(ProtocolID).ToContain("/sbc/") + }) + + gest.Register(s) +} diff --git a/internal/p2p/peer.go b/internal/p2p/peer.go new file mode 100644 index 0000000..3bfa3f8 --- /dev/null +++ b/internal/p2p/peer.go @@ -0,0 +1,47 @@ +package p2p + +import ( + "bufio" + "encoding/json" + "fmt" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" +) + +type Peer struct { + ID peer.ID + Stream network.Stream + BestHeight int +} + +func NewPeer(stream network.Stream, id peer.ID) *Peer { + return &Peer{ + ID: id, + Stream: stream, + } +} + +func (p *Peer) SendMessage(msg *Message) error { + data, err := json.Marshal(msg) + if err != nil { + return err + } + _, err = p.Stream.Write(append(data, '\n')) + return err +} + +func (p *Peer) ReadMessage() (*Message, error) { + reader := bufio.NewReader(p.Stream) + data, err := reader.ReadBytes('\n') + if err != nil { + return nil, err + } + var msg Message + err = json.Unmarshal(data, &msg) + return &msg, err +} + +func (p *Peer) String() string { + return fmt.Sprintf("Peer{%s}", p.ID) +} diff --git a/internal/p2p/server.go b/internal/p2p/server.go new file mode 100644 index 0000000..96da67d --- /dev/null +++ b/internal/p2p/server.go @@ -0,0 +1,714 @@ +package p2p + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "strings" + "sync" + "time" + + "KitsuneSemCalda/SBC/internal/blockchain" + "KitsuneSemCalda/SBC/internal/p2p/callbacks" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +type Server struct { + host *Host + blockchain *blockchain.Blockchain + peers map[peer.ID]*Peer + peerMutex sync.RWMutex + config *Config + + peer_callback callbacks.PeerCallbacks + block_callback callbacks.BlockCallbacks + + processedMutex sync.RWMutex + processedBlocks map[string]time.Time +} + +func NewServer(cfg *Config, bc *blockchain.Blockchain) (*Server, error) { + s := &Server{ + blockchain: bc, + peers: make(map[peer.ID]*Peer), + config: cfg, + processedBlocks: make(map[string]time.Time), + } + + host, err := NewHost(cfg, s) + if err != nil { + return nil, err + } + s.host = host + + s.host.SetStreamHandler(s.handleStream) + + go func() { + time.Sleep(2 * time.Second) + s.tryAutoConnect() + }() + + s.blockchain.Subscribe(func(block *blockchain.Block) { + s.processedMutex.Lock() + if !s.processedBlocks[block.Hash].IsZero() { + s.processedMutex.Unlock() + return + } + s.processedBlocks[block.Hash] = time.Now() + s.processedMutex.Unlock() + s.BroadcastBlock(block) + }) + + go s.cleanupProcessedBlocks() + + return s, nil +} + +func (s *Server) cleanupProcessedBlocks() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + now := time.Now() + s.processedMutex.Lock() + for hash, timestamp := range s.processedBlocks { + if now.Sub(timestamp) > 10*time.Minute { + delete(s.processedBlocks, hash) + } + } + s.processedMutex.Unlock() + } +} + +func (s *Server) handleStream(stream network.Stream) { + pID := stream.Conn().RemotePeer() + + s.peerMutex.Lock() + if _, exists := s.peers[pID]; exists { + s.peerMutex.Unlock() + stream.Reset() + return + } + + peer := NewPeer(stream, pID) + s.peers[pID] = peer + s.peerMutex.Unlock() + + if s.peer_callback != nil { + s.peer_callback.OnNewPeer(pID) + } + + go s.readMessages(peer) + s.sendVersion(peer) +} + +func (s *Server) readMessages(p *Peer) { + defer func() { + s.peerMutex.Lock() + delete(s.peers, p.ID) + s.peerMutex.Unlock() + + if s.peer_callback != nil { + s.peer_callback.OnDisconnect(p.ID) + } + }() + + for { + msg, err := p.ReadMessage() + if err != nil { + return + } + s.handleMessage(p, msg) + } +} + +func (s *Server) handleMessage(p *Peer, msg *Message) { + switch msg.Type { + case MsgVersion: + var payload VersionPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling version: %v", err) + return + } + p.BestHeight = payload.BestHeight + verAck, _ := NewMessage(MsgVerAck, VerAckPayload{Accept: true}) + p.SendMessage(verAck) + case MsgVerAck: + log.Printf("Handshake completo com peer: %s", p.ID) + if p.BestHeight > s.blockchain.Length() { + s.sendGetBlocks(p) + } + msg, _ := NewMessage(MsgGetPeers, GetPeersPayload{}) + p.SendMessage(msg) + case MsgBlock: + var payload BlockPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + return + } + + block := &blockchain.Block{ + Index: payload.Index, + Timestamp: payload.Timestamp, + BPM: payload.BPM, + Hash: payload.Hash, + PrevHash: payload.PrevHash, + } + + // Avoid processing if already handled + s.processedMutex.RLock() + if !s.processedBlocks[block.Hash].IsZero() { + s.processedMutex.RUnlock() + return + } + s.processedMutex.RUnlock() + + if s.block_callback != nil { + s.block_callback.OnBlockReceived(block) + } + + s.blockchain.ProcessBlock(block) + s.processedMutex.Lock() + s.processedBlocks[block.Hash] = time.Now() + s.processedMutex.Unlock() + case MsgGetBlocks: + var payload GetBlocksPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling getblocks: %v", err) + return + } + + blocks := s.blockchain.GetBlocksAfter(payload.StartHash, 500) + if len(blocks) > 0 { + var invVecs []InvVec + for _, b := range blocks { + invVecs = append(invVecs, InvVec{Type: "block", Hash: b.Hash}) + } + inv := InvPayload{Count: len(invVecs), InvVec: invVecs} + msg, _ := NewMessage(MsgInv, inv) + p.SendMessage(msg) + } + case MsgInv: + var payload InvPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling inv: %v", err) + return + } + + var missingVecs []InvVec + s.processedMutex.RLock() + for _, vec := range payload.InvVec { + if vec.Type == "block" && s.processedBlocks[vec.Hash].IsZero() { + missingVecs = append(missingVecs, vec) + } + } + s.processedMutex.RUnlock() + + if len(missingVecs) > 0 { + msg, _ := NewMessage(MsgGetData, InvPayload{ + Count: len(missingVecs), + InvVec: missingVecs, + }) + p.SendMessage(msg) + } + case MsgGetData: + var payload InvPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling getdata: %v", err) + return + } + + for _, vec := range payload.InvVec { + if vec.Type == "block" { + block := s.blockchain.GetBlockByHash(vec.Hash) + if block != nil { + bPayload := BlockPayload{ + Index: block.Index, + Timestamp: block.Timestamp, + BPM: block.BPM, + Hash: block.Hash, + PrevHash: block.PrevHash, + } + msg, _ := NewMessage(MsgBlock, bPayload) + p.SendMessage(msg) + } + } + } + case MsgGetPeers: + var peers []string + for _, peer := range s.peers { + for _, addr := range s.host.Addrs() { + peers = append(peers, addr.String()+"/p2p/"+peer.ID.String()) + } + } + peersMsg, _ := NewMessage(MsgPeers, PeersPayload{Peers: peers}) + p.SendMessage(peersMsg) + case MsgPeers: + var payload PeersPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling peers: %v", err) + return + } + for _, addr := range payload.Peers { + if addr == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + continue + } + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + continue + } + ctx := context.Background() + if err := s.host.Connect(ctx, ma); err != nil { + continue + } + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + continue + } + s.handleStream(stream) + } + case MsgFindBlock: + var payload FindBlockPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling findblock: %v", err) + return + } + block := s.blockchain.GetBlockByHash(payload.Hash) + if block != nil { + bPayload := BlockPayload{ + Index: block.Index, + Timestamp: block.Timestamp, + BPM: block.BPM, + Hash: block.Hash, + PrevHash: block.PrevHash, + } + resp, _ := NewMessage(MsgBlockFound, BlockFoundPayload{Found: true, Block: &bPayload}) + p.SendMessage(resp) + } else { + resp, _ := NewMessage(MsgBlockFound, BlockFoundPayload{Found: false}) + p.SendMessage(resp) + } + case MsgBlockFound: + var payload BlockFoundPayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + log.Printf("Error unmarshaling blockfound: %v", err) + return + } + if payload.Found && payload.Block != nil { + block := &blockchain.Block{ + Index: payload.Block.Index, + Timestamp: payload.Block.Timestamp, + BPM: payload.Block.BPM, + Hash: payload.Block.Hash, + PrevHash: payload.Block.PrevHash, + } + s.blockchain.ProcessBlock(block) + log.Printf("[FIND] Block #%d found!", block.Index) + } + } +} + +func (s *Server) BroadcastBlock(block *blockchain.Block) { + s.processedMutex.Lock() + s.processedBlocks[block.Hash] = time.Now() + s.processedMutex.Unlock() + payload := BlockPayload{ + Index: block.Index, + Timestamp: block.Timestamp, + BPM: block.BPM, + Hash: block.Hash, + PrevHash: block.PrevHash, + } + msg, _ := NewMessage(MsgBlock, payload) + + s.peerMutex.RLock() + defer s.peerMutex.RUnlock() + for _, p := range s.peers { + p.SendMessage(msg) + } +} + +func (s *Server) sendVersion(p *Peer) { + payload := VersionPayload{ + Version: 1, + Timestamp: time.Now(), + BestHeight: s.blockchain.Length(), + } + msg, _ := NewMessage(MsgVersion, payload) + p.SendMessage(msg) +} + +func (s *Server) sendGetBlocks(p *Peer) { + lastBlock := s.blockchain.GetLastBlock() + payload := GetBlocksPayload{ + StartHash: lastBlock.Hash, + } + msg, _ := NewMessage(MsgGetBlocks, payload) + p.SendMessage(msg) +} + +func (s *Server) sendInv(p *Peer) { + lastBlock := s.blockchain.GetLastBlock() + inv := InvPayload{ + Count: 1, + InvVec: []InvVec{ + {Type: "block", Hash: lastBlock.Hash}, + }, + } + msg, _ := NewMessage(MsgInv, inv) + p.SendMessage(msg) +} + +func (s *Server) SetBlockCallback(cb callbacks.BlockCallbacks) { + s.block_callback = cb +} + +func (s *Server) SetPeerCallback(cb callbacks.PeerCallbacks) { + s.peer_callback = cb + if s.host != nil { + s.host.SetPeerCallback(s) + } +} + +func (s *Server) HandlePeerFound(pi peer.AddrInfo) { + if pi.ID == s.host.ID() { + return + } + + s.peerMutex.RLock() + if _, connected := s.peers[pi.ID]; connected { + s.peerMutex.RUnlock() + return + } + s.peerMutex.RUnlock() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := s.host.ConnectPeer(ctx, pi); err != nil { + fmt.Printf("[P2P] Failed to connect to %s: %v\n", pi.ID, err) + return + } + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + fmt.Printf("[P2P] Failed to open stream to %s: %v\n", pi.ID, err) + return + } + s.handleStream(stream) +} + +func (s *Server) OnPeerFound(pi peer.AddrInfo) { + s.HandlePeerFound(pi) +} + +func (s *Server) ConnectToPeer(addr string) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + return err + } + + err = s.host.Connect(ctx, ma) + if err != nil { + return err + } + + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + return err + } + + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + return err + } + + s.handleStream(stream) + return nil +} + +func (s *Server) Start(ctx context.Context) error { + <-ctx.Done() + return nil +} + +func (s *Server) GetHostID() string { + return s.host.ID().String() +} + +func (s *Server) GetAddrs() []multiaddr.Multiaddr { + return s.host.Addrs() +} + +func (s *Server) Close() error { + return s.host.Close() +} + +func (s *Server) GetPeers() map[peer.ID]*Peer { + s.peerMutex.RLock() + defer s.peerMutex.RUnlock() + + peersCopy := make(map[peer.ID]*Peer) + for id, p := range s.peers { + peersCopy[id] = p + } + return peersCopy +} + +func (s *Server) RequestSync() { + s.peerMutex.RLock() + defer s.peerMutex.RUnlock() + for _, p := range s.peers { + msg, _ := NewMessage(MsgGetBlocks, GetBlocksPayload{StartHash: ""}) + p.SendMessage(msg) + } +} + +func (s *Server) DiscoverPeers() { + s.peerMutex.RLock() + defer s.peerMutex.RUnlock() + for _, p := range s.peers { + msg, _ := NewMessage(MsgGetPeers, GetPeersPayload{}) + p.SendMessage(msg) + } +} + +func (s *Server) FindBlock(hash string) { + s.peerMutex.RLock() + defer s.peerMutex.RUnlock() + for _, p := range s.peers { + msg, _ := NewMessage(MsgFindBlock, FindBlockPayload{Hash: hash}) + p.SendMessage(msg) + } +} + +func (s *Server) tryAutoConnect() { + s.tryLocalDiscovery() + + if len(s.config.BootNode) > 0 { + go s.connectToBootNodes() + } + + if s.config.DNSSeed != "" { + go s.resolveDNSSeeds(s.config.DNSSeed) + seeds := strings.Split(s.config.DNSSeed, ",") + for _, seed := range seeds { + seed = strings.TrimSpace(seed) + if seed == "" { + continue + } + go s.fetchSeedsFromHTTP(fmt.Sprintf("http://%s:8080/seeds", seed)) + } + } + + go s.periodicPeerDiscovery() +} + +func (s *Server) connectToBootNodes() { + for _, addr := range s.config.BootNode { + addr = strings.TrimSpace(addr) + if addr == "" { + continue + } + + fmt.Printf("[BOOT] Trying to connect to bootnode: %s\n", addr) + + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + fmt.Printf("[BOOT] Invalid multiaddr %s: %v\n", addr, err) + continue + } + + ctx := context.Background() + if err := s.host.Connect(ctx, ma); err != nil { + fmt.Printf("[BOOT] Could not connect to %s: %v\n", addr, err) + continue + } + + pi, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + continue + } + + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + fmt.Printf("[BOOT] Could not open stream to %s: %v\n", addr, err) + continue + } + + s.handleStream(stream) + fmt.Printf("[BOOT] Connected to bootnode: %s\n", addr) + } +} + +func (s *Server) periodicPeerDiscovery() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + <-ticker.C + if len(s.peers) > 0 { + fmt.Printf("[DISCOVERY] Periodic peer discovery (connected peers: %d)\n", len(s.peers)) + s.DiscoverPeers() + } + } +} + +func (s *Server) tryLocalDiscovery() { + announceFile := "/tmp/sbc-daemon.json" + data, err := os.ReadFile(announceFile) + if err != nil { + return + } + + var daemon struct { + Addr string `json:"addr"` + PeerID string `json:"peer_id"` + } + if err := json.Unmarshal(data, &daemon); err != nil { + return + } + + if daemon.Addr != "" && daemon.PeerID != "" { + daemonAddr := daemon.Addr + "/p2p/" + daemon.PeerID + + localIP := s.getLocalIP() + if localIP != "" { + daemonAddr = strings.Replace(daemonAddr, "/ip4/0.0.0.0/", "/ip4/"+localIP+"/", 1) + daemonAddr = strings.Replace(daemonAddr, "/ip4/127.0.0.1/", "/ip4/"+localIP+"/", 1) + } + + ma, err := multiaddr.NewMultiaddr(daemonAddr) + if err == nil { + ctx := context.Background() + if err := s.host.Connect(ctx, ma); err == nil { + pi, _ := peer.AddrInfoFromP2pAddr(ma) + if stream, err := s.host.NewStream(ctx, pi.ID); err == nil { + s.handleStream(stream) + fmt.Printf("[AUTO] Connected to local daemon: %s\n", daemonAddr) + return + } + } + } + } +} + +func (s *Server) getLocalIP() string { + addrs := s.host.Addrs() + for _, addr := range addrs { + parts := strings.Split(addr.String(), "/") + for _, part := range parts { + if ip := net.ParseIP(part); ip != nil && ip.To4() != nil { + return part + } + } + } + return "" +} + +func (s *Server) resolveDNSSeeds(dnsSeed string) { + seeds := strings.Split(dnsSeed, ",") + for _, seed := range seeds { + seed = strings.TrimSpace(seed) + if seed == "" { + continue + } + + fmt.Printf("[DNS] Resolving seed: %s\n", seed) + + ips, err := net.LookupIP(seed) + if err != nil { + fmt.Printf("[DNS] Failed to resolve %s: %v\n", seed, err) + continue + } + + for _, ip := range ips { + addr := fmt.Sprintf("/ip4/%s/tcp/8333", ip.String()) + ma, err := multiaddr.NewMultiaddr(addr) + if err != nil { + continue + } + + ctx := context.Background() + if err := s.host.Connect(ctx, ma); err != nil { + fmt.Printf("[DNS] Could not connect to %s: %v\n", addr, err) + continue + } + + pi, _ := peer.AddrInfoFromP2pAddr(ma) + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + fmt.Printf("[DNS] Could not open stream to %s: %v\n", addr, err) + continue + } + + s.handleStream(stream) + fmt.Printf("[DNS] Connected to bootstrap node: %s\n", addr) + return + } + } +} + +func (s *Server) fetchSeedsFromHTTP(seedURL string) { + resp, err := http.Get(seedURL) + if err != nil { + fmt.Printf("[HTTP SEED] Failed to fetch %s: %v\n", seedURL, err) + return + } + defer resp.Body.Close() + + var result struct { + Peers []struct { + Addr string `json:"addr"` + } `json:"peers"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + fmt.Printf("[HTTP SEED] Failed to decode response: %v\n", err) + return + } + + if len(result.Peers) == 0 { + fmt.Printf("[HTTP SEED] No peers found at %s\n", seedURL) + return + } + + fmt.Printf("[HTTP SEED] Found %d peers from %s\n", len(result.Peers), seedURL) + + for _, p := range result.Peers { + if p.Addr == "" { + continue + } + + ma, err := multiaddr.NewMultiaddr(p.Addr) + if err != nil { + continue + } + + ctx := context.Background() + if err := s.host.Connect(ctx, ma); err != nil { + continue + } + + pi, _ := peer.AddrInfoFromP2pAddr(ma) + stream, err := s.host.NewStream(ctx, pi.ID) + if err != nil { + continue + } + + s.handleStream(stream) + fmt.Printf("[HTTP SEED] Connected to peer: %s\n", p.Addr) + return + } +} diff --git a/internal/p2p/server_sync_spec.go b/internal/p2p/server_sync_spec.go new file mode 100644 index 0000000..6c48529 --- /dev/null +++ b/internal/p2p/server_sync_spec.go @@ -0,0 +1,55 @@ +package p2p + +import ( + "context" + "time" + + "KitsuneSemCalda/SBC/internal/blockchain" + + "github.com/caiolandgraf/gest/gest" +) + +func init() { + s := gest.Describe("P2P Synchronization") + + s.It("should synchronize blocks between two nodes", func(t *gest.T) { + bcA := blockchain.NewBlockchain() + for i := 1; i <= 5; i++ { + bcA.AddBlock(i * 10) + } + + cfgA := &Config{ + ListenAddr: "/ip4/127.0.0.1/tcp/0", + } + serverA, err := NewServer(cfgA, bcA) + t.Expect(err).ToBeNil() + + ctxA, cancelA := context.WithCancel(context.Background()) + defer cancelA() + go serverA.Start(ctxA) + + bcB := blockchain.NewBlockchain() + cfgB := &Config{ + ListenAddr: "/ip4/127.0.0.1/tcp/0", + } + serverB, err := NewServer(cfgB, bcB) + t.Expect(err).ToBeNil() + + ctxB, cancelB := context.WithCancel(context.Background()) + defer cancelB() + go serverB.Start(ctxB) + + time.Sleep(100 * time.Millisecond) // Give servers time to start + + addrA := serverA.GetAddrs()[0].String() + "/p2p/" + serverA.GetHostID() + err = serverB.ConnectToPeer(addrA) + t.Expect(err).ToBeNil() + + // Wait for synchronization + time.Sleep(2 * time.Second) + + t.Expect(bcB.Length()).ToBe(6) // Genesis + 5 blocks + }) + + gest.Register(s) +} diff --git a/internal/storage/store.go b/internal/storage/store.go new file mode 100644 index 0000000..619e2db --- /dev/null +++ b/internal/storage/store.go @@ -0,0 +1,101 @@ +package storage + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "time" + + "KitsuneSemCalda/SBC/internal/blockchain" + + _ "github.com/mattn/go-sqlite3" +) + +type Store struct { + db *sql.DB +} + +func NewStore(dataDir string) (*Store, error) { + if dataDir == "" { + dataDir = "." + } + + err := os.MkdirAll(dataDir, 0755) + if err != nil { + return nil, fmt.Errorf("failed to create data directory: %w", err) + } + + dbPath := filepath.Join(dataDir, "blockchain.db") + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + query := ` + CREATE TABLE IF NOT EXISTS blocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + block_index INTEGER NOT NULL, + timestamp TEXT NOT NULL, + bpm INTEGER NOT NULL, + hash TEXT NOT NULL UNIQUE, + prev_hash TEXT NOT NULL + );` + + _, err = db.Exec(query) + if err != nil { + return nil, fmt.Errorf("failed to create table: %w", err) + } + + return &Store{db: db}, nil +} + +func (s *Store) Save(bc *blockchain.Blockchain) error { + blocks := bc.GetAllBlocks() + tx, err := s.db.Begin() + if err != nil { + return err + } + + stmt, err := tx.Prepare("INSERT OR IGNORE INTO blocks(block_index, timestamp, bpm, hash, prev_hash) VALUES(?, ?, ?, ?, ?)") + if err != nil { + return err + } + defer stmt.Close() + + for _, b := range blocks { + _, err = stmt.Exec(b.Index, b.Timestamp.Format(time.RFC3339Nano), b.BPM, b.Hash, b.PrevHash) + if err != nil { + tx.Rollback() + return err + } + } + + return tx.Commit() +} + +func (s *Store) Load(bc *blockchain.Blockchain) error { + query := "SELECT block_index, timestamp, bpm, hash, prev_hash FROM blocks ORDER BY block_index ASC" + rows, err := s.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var b blockchain.Block + var ts string + err = rows.Scan(&b.Index, &ts, &b.BPM, &b.Hash, &b.PrevHash) + if err != nil { + return err + } + b.Timestamp, _ = time.Parse(time.RFC3339Nano, ts) + bc.ProcessBlock(&b) + } + + return nil +} + +func (s *Store) Close() error { + return s.db.Close() +}