Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
34fb433
build: Go
KitsuneSemCalda Feb 27, 2026
86e4145
feat: structures
KitsuneSemCalda Feb 27, 2026
6e2f830
feat: blockchain_node
KitsuneSemCalda Feb 27, 2026
72a979c
feat: blockchain
KitsuneSemCalda Feb 27, 2026
efa9bf5
feat(p2p): add a initial p2p server to communicate between nodes
KitsuneSemCalda Feb 28, 2026
871328a
build(deps): add libp2p as dependency
KitsuneSemCalda Feb 28, 2026
096ba07
feat(blockchain): add a method to get all blocks
KitsuneSemCalda Feb 28, 2026
43fa2b0
chore(gitignore): remove compiled version of repository
KitsuneSemCalda Feb 28, 2026
aebe06a
build(make): add a Makefile to build the blockchain
KitsuneSemCalda Feb 28, 2026
fcd844c
feat(main): add two binaries from client and daemon
KitsuneSemCalda Feb 28, 2026
5f03a7c
feat(p2p): add callbacks to network
KitsuneSemCalda Feb 28, 2026
0bf54ef
test(gest): add gest as default tester
KitsuneSemCalda Feb 28, 2026
d0f72d6
test(gest): add tester initial config
KitsuneSemCalda Feb 28, 2026
e0b3280
build(make): use make to run tests
KitsuneSemCalda Feb 28, 2026
4763f05
feat(cmd): try initialize the common server between nodes
KitsuneSemCalda Feb 28, 2026
b35ac9b
ci(tester): run tester on CI
KitsuneSemCalda Feb 28, 2026
ee69799
refactor(mv): improve the order of source code
KitsuneSemCalda Feb 28, 2026
d093643
chore(gitignore): ignore sqlite3 db
KitsuneSemCalda Feb 28, 2026
038349d
feat: implementation of robust block validation, chain reorg, and com…
KitsuneSemCalda Mar 1, 2026
e7ccc21
feat: implementation of centralized logging, failed peers cache, and …
KitsuneSemCalda Mar 2, 2026
6426a8a
Merge branch 'master' into dev
KitsuneSemCalda Mar 2, 2026
6b66875
test(sbc): update tests to work properly
KitsuneSemCalda Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Tests/unit/p2p/host_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package p2p_tests

import (
"KitsuneSemCalda/SBC/internal/p2p"
"github.com/caiolandgraf/gest/gest"
"strings"
)

func init() {
s := gest.Describe("P2P Host Core")

s.It("should detect if a public IP is valid format", func(t *gest.T) {
// This might fail if no internet, so we test the logic via helper if available
// or just check if it returns something or an error
ip, _ := p2p.GetPublicIP()
if ip != "" {
parts := strings.Split(ip, ".")
t.Expect(len(parts)).ToBe(4)
}
})

s.It("should include public IP in multiaddrs if set", func(t *gest.T) {
cfg := p2p.DefaultConfig()
h, _ := p2p.NewHost(cfg, nil)

addrs := h.Addrs()
t.Expect(len(addrs)).Not().ToBe(0)

h.Close()
})

gest.Register(s)
}
21 changes: 21 additions & 0 deletions Tests/unit/p2p/logger_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package p2p_tests

import (
"KitsuneSemCalda/SBC/internal/p2p"
"github.com/caiolandgraf/gest/gest"
)

func init() {
s := gest.Describe("P2P Logger")

s.It("should respect log levels", func(t *gest.T) {
p2p.SetLogLevel(p2p.LevelError)
p2p.Debug("TEST", "This should not appear")
p2p.Info("TEST", "This should not appear")
p2p.Error("TEST", "This is an error")

t.Expect(true).ToBeTrue()
})

gest.Register(s)
}
53 changes: 53 additions & 0 deletions Tests/unit/p2p/server_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package p2p_tests

import (
"context"
"KitsuneSemCalda/SBC/internal/blockchain"
"KitsuneSemCalda/SBC/internal/p2p"
"github.com/caiolandgraf/gest/gest"
"time"
)

func init() {
s := gest.Describe("P2P Server Core")

s.It("should manage failed peers cache", func(t *gest.T) {
bc := blockchain.NewBlockchain()
cfg := p2p.DefaultConfig()
server, _ := p2p.NewServer(cfg, bc)

// Valid multihash for a peer ID
addr := "/ip4/1.1.1.1/tcp/8333/p2p/12D3KooWLGpBKHhshKgGJiiKxKUPKJVBsE3w1td6jSPTquU1xCjk"

// Attempt connection to non-existent but valid addr (will fail and mark)
err := server.ConnectToPeer(addr)
t.Expect(err).Not().ToBeNil()

// Second attempt should return nil quickly (cached failure)
start := time.Now()
err = server.ConnectToPeer(addr)
duration := time.Since(start)

t.Expect(err).ToBeNil()
t.Expect(duration < 100*time.Millisecond).ToBeTrue()

server.Close()
})

s.It("should start maintenance tasks without crashing", func(t *gest.T) {
bc := blockchain.NewBlockchain()
cfg := p2p.DefaultConfig()
server, _ := p2p.NewServer(cfg, bc)

ctx, cancel := context.WithCancel(context.Background())
server.StartMaintenance(ctx)

time.Sleep(50 * time.Millisecond)
cancel()

t.Expect(true).ToBeTrue()
server.Close()
})

gest.Register(s)
}
86 changes: 1 addition & 85 deletions cmd/sbcd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ import (

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
Expand Down Expand Up @@ -96,84 +90,6 @@ func (h *ColoredHandler) Handle(ctx context.Context, r slog.Record) error {
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")
Expand Down Expand Up @@ -220,7 +136,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

startMaintenance(ctx, bc, server)
server.StartMaintenance(ctx)

stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
Expand Down
15 changes: 15 additions & 0 deletions internal/p2p/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"

Expand Down Expand Up @@ -305,3 +307,16 @@ func (h *Host) Close() error {
}
return h.host.Close()
}

func GetPublicIP() (string, error) {
resp, err := http.Get("https://api.ipify.org")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
72 changes: 72 additions & 0 deletions internal/p2p/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package p2p

import (
"fmt"
"io"
"os"
"sync"
"time"
)

type Level int

const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
LevelNone
)

type Logger struct {
level Level
output io.Writer
mu sync.Mutex
}

var defaultLogger = &Logger{
level: LevelInfo,
output: os.Stdout,
}

func (l *Logger) SetLevel(level Level) {
l.mu.Lock()
defer l.mu.Unlock()
l.level = level
}

func (l *Logger) log(level Level, prefix string, format string, v ...any) {
l.mu.Lock()
defer l.mu.Unlock()
if level < l.level {
return
}

msg := fmt.Sprintf(format, v...)
timestamp := time.Now().Format("15:04:05")

var colorCode string
switch level {
case LevelDebug:
colorCode = "\033[36m" // Cyan
case LevelInfo:
colorCode = "\033[32m" // Green
case LevelWarn:
colorCode = "\033[33m" // Yellow
case LevelError:
colorCode = "\033[31m" // Red
}

if colorCode != "" {
fmt.Fprintf(l.output, "%s[%s] %s %s\033[0m\n", colorCode, prefix, timestamp, msg)
} else {
fmt.Fprintf(l.output, "[%s] %s %s\n", prefix, timestamp, msg)
}
}

func Debug(prefix, format string, v ...any) { defaultLogger.log(LevelDebug, prefix, format, v...) }
func Info(prefix, format string, v ...any) { defaultLogger.log(LevelInfo, prefix, format, v...) }
func Warn(prefix, format string, v ...any) { defaultLogger.log(LevelWarn, prefix, format, v...) }
func Error(prefix, format string, v ...any) { defaultLogger.log(LevelError, prefix, format, v...) }

func SetLogLevel(level Level) { defaultLogger.SetLevel(level) }
Loading