From c2e60c6ecef6a71c7aeef0b51f7c6722288be3ac Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:20:17 +0000 Subject: [PATCH 01/12] refactor(logging): prefer get-style logger accessors Co-Authored-By: Virgil --- logging/compat.go | 20 ++++++++++---------- logging/logger.go | 28 ++++++++++++++-------------- logging/logger_test.go | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/logging/compat.go b/logging/compat.go index 404910c..74f014b 100644 --- a/logging/compat.go +++ b/logging/compat.go @@ -10,18 +10,18 @@ func (l *Logger) WithComponent(component string) *Logger { return l.ComponentLogger(component) } -// GetLevel returns the current log level. -// Preferred over Level for AX naming consistency. +// Level returns the current log level. +// Deprecated: Use GetLevel instead. // -// level := logger.GetLevel() -func (l *Logger) GetLevel() Level { - return l.Level() +// level := logger.Level() +func (l *Logger) Level() Level { + return l.GetLevel() } -// GetGlobal returns the global logger instance. -// Deprecated: Use Global instead. +// Global returns the global logger instance. +// Deprecated: Use GetGlobal instead. // -// logger := GetGlobal() -func GetGlobal() *Logger { - return Global() +// logger := Global() +func Global() *Logger { + return GetGlobal() } diff --git a/logging/logger.go b/logging/logger.go index a24c227..2746881 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -107,10 +107,10 @@ func (l *Logger) SetLevel(level Level) { l.level = level } -// Level returns the current log level. +// GetLevel returns the current log level. // -// level := logger.Level() -func (l *Logger) Level() Level { +// level := logger.GetLevel() +func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level @@ -242,10 +242,10 @@ func SetGlobal(l *Logger) { globalLogger = l } -// Global returns the global logger instance. +// GetGlobal returns the global logger instance. // -// logger := Global() -func Global() *Logger { +// logger := GetGlobal() +func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger @@ -266,56 +266,56 @@ func SetGlobalLevel(level Level) { // // Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { - Global().Debug(message, fields...) + GetGlobal().Debug(message, fields...) } // Info logs an informational message using the global logger. // // Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { - Global().Info(message, fields...) + GetGlobal().Info(message, fields...) } // Warn logs a warning message using the global logger. // // Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { - Global().Warn(message, fields...) + GetGlobal().Warn(message, fields...) } // Error logs an error message using the global logger. // // Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { - Global().Error(message, fields...) + GetGlobal().Error(message, fields...) } // Debugf logs a formatted debug message using the global logger. // // Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { - Global().Debugf(format, args...) + GetGlobal().Debugf(format, args...) } // Infof logs a formatted informational message using the global logger. // // Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { - Global().Infof(format, args...) + GetGlobal().Infof(format, args...) } // Warnf logs a formatted warning message using the global logger. // // Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { - Global().Warnf(format, args...) + GetGlobal().Warnf(format, args...) } // Errorf logs a formatted error message using the global logger. // // Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { - Global().Errorf(format, args...) + GetGlobal().Errorf(format, args...) } // ParseLevel parses a string into a log level. diff --git a/logging/logger_test.go b/logging/logger_test.go index 08c48a9..b46aaec 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -149,8 +149,8 @@ func TestLogger_SetLevel_Good(t *testing.T) { } // Verify Level - if logger.Level() != LevelInfo { - t.Error("Level should return LevelInfo") + if logger.GetLevel() != LevelInfo { + t.Error("GetLevel should return LevelInfo") } } From 711a43aa3f91a068b5917219973fd2812776fd70 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:26:00 +0000 Subject: [PATCH 02/12] refactor(node): remove AX compatibility aliases Co-Authored-By: Virgil --- docs/architecture.md | 8 ++-- docs/development.md | 2 +- docs/history.md | 4 +- docs/transport.md | 12 +++--- docs/ueps.md | 4 +- logging/compat.go | 27 ------------ logging/compat_test.go | 39 ------------------ node/compat.go | 46 --------------------- node/compat_test.go | 57 ------------------------- node/levin/compat.go | 87 --------------------------------------- node/levin/compat_test.go | 66 ----------------------------- specs/logging.md | 5 +-- specs/node-levin.md | 34 +++++++-------- specs/node.md | 11 ++--- ueps/compat.go | 11 ----- ueps/compat_test.go | 14 ------- 16 files changed, 38 insertions(+), 389 deletions(-) delete mode 100644 logging/compat.go delete mode 100644 logging/compat_test.go delete mode 100644 node/compat.go delete mode 100644 node/compat_test.go delete mode 100644 node/levin/compat.go delete mode 100644 node/levin/compat_test.go delete mode 100644 ueps/compat.go delete mode 100644 ueps/compat_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 105c0ec..1f9f3a2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,8 +37,8 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| | `ListenAddr` | `:9091` | HTTP bind address | -| `WSPath` | `/ws` | WebSocket endpoint | -| `MaxConns` | 100 | Maximum concurrent connections | +| `WebSocketPath` | `/ws` | WebSocket endpoint | +| `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | | `PingInterval` | 30 s | Keepalive ping period | | `PongTimeout` | 10 s | Maximum time to wait for pong | @@ -56,7 +56,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Rate limiting**: Each `PeerConnection` holds a `PeerRateLimiter` (token bucket: 100 burst, 50 tokens/second refill). Messages from rate-limited peers are dropped in the read loop. -**MaxConns enforcement**: The handler tracks `pendingConns` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. +**MaxConnections enforcement**: The handler tracks `pendingHandshakeCount` (atomic counter) during the handshake phase in addition to established connections, preventing races where a surge of simultaneous inbounds could exceed the limit. **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. @@ -244,7 +244,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. | `Controller.pending` | `sync.RWMutex` | | `MessageDeduplicator.seen` | `sync.RWMutex` | | `Dispatcher.handlers` | `sync.RWMutex` | -| `Transport.pendingConns` | `atomic.Int32` | +| `Transport.pendingHandshakeCount` | `atomic.Int32` | The codebase is verified race-free under `go test -race`. diff --git a/docs/development.md b/docs/development.md index 8d2bd3b..196c370 100644 --- a/docs/development.md +++ b/docs/development.md @@ -233,7 +233,7 @@ Examples: ``` feat(dispatcher): implement UEPS threat circuit breaker -test(transport): add keepalive timeout and MaxConns enforcement tests +test(transport): add keepalive timeout and MaxConnections enforcement tests fix(peer): prevent data race in GracefulClose (P2P-RACE-1) ``` diff --git a/docs/history.md b/docs/history.md index 52ea3f2..5c42f56 100644 --- a/docs/history.md +++ b/docs/history.md @@ -28,7 +28,7 @@ Tests covered: - Encrypted message round-trip: SMSG encrypt on one side, decrypt on other - Message deduplication: duplicate UUID dropped silently - Rate limiting: burst of more than 100 messages, subsequent drops after token bucket empties -- MaxConns enforcement: 503 HTTP rejection when limit is reached +- MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close - Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) @@ -92,7 +92,7 @@ The `TagPayload` (0xFF) field now uses the same 2-byte length prefix as the othe ### No Resource Cleanup on Some Error Paths -`transport.handleWSUpgrade` does not clean up on handshake timeout (the `pendingConns` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). +`transport.handleWebSocketUpgrade` does not clean up on handshake timeout (the `pendingHandshakeCount` counter is decremented correctly via `defer`, but the underlying WebSocket connection may linger briefly before the read deadline fires). `transport.Connect` does not clean up the temporary connection object on handshake failure (the raw WebSocket `conn` is closed, but there is no registry or metrics cleanup for the partially constructed `PeerConnection`). These are low-severity gaps. They do not cause goroutine leaks under the current implementation because the connection's read loop is not started until after a successful handshake. diff --git a/docs/transport.md b/docs/transport.md index 19a7987..5aad542 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -12,10 +12,10 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { ListenAddr string // ":9091" default - WSPath string // "/ws" -- WebSocket endpoint path + WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string - MaxConns int // Maximum concurrent connections (default 100) + MaxConnections int // Maximum concurrent connections (default 100) MaxMessageSize int64 // Maximum message size in bytes (default 1MB) PingInterval time.Duration // Keepalive interval (default 30s) PongTimeout time.Duration // Pong wait timeout (default 10s) @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go cfg := node.DefaultTransportConfig() -// ListenAddr: ":9091", WSPath: "/ws", MaxConns: 100 +// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -123,9 +123,9 @@ const ( ## Incoming Connections -The transport exposes an HTTP handler at the configured `WSPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. +The transport exposes an HTTP handler at the configured `WebSocketPath` that upgrades to WebSocket. Origin checks restrict browser clients to `localhost`, `127.0.0.1`, and `::1`; non-browser clients (no `Origin` header) are allowed. -The `MaxConns` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. +The `MaxConnections` limit is enforced before the WebSocket upgrade, counting both established and pending (mid-handshake) connections. Excess connections receive HTTP 503. ## Message Deduplication @@ -166,7 +166,7 @@ err = transport.Send(peerID, msg) err = transport.Broadcast(msg) // Query connections -count := transport.ConnectedPeers() +count := transport.ConnectedPeerCount() conn := transport.GetConnection(peerID) // Iterate over all connections diff --git a/docs/ueps.md b/docs/ueps.md index 2ba184a..09c0b71 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -25,8 +25,8 @@ Each field is encoded as a 1-byte tag, 2-byte big-endian length (uint16), and va | Tag | Constant | Value Size | Description | |-----|----------|------------|-------------| | `0x01` | `TagVersion` | 1 byte | Protocol version (default `0x09` for IPv9) | -| `0x02` | `TagCurrentLay` | 1 byte | Current network layer | -| `0x03` | `TagTargetLay` | 1 byte | Target network layer | +| `0x02` | `TagCurrentLayer` | 1 byte | Current network layer | +| `0x03` | `TagTargetLayer` | 1 byte | Target network layer | | `0x04` | `TagIntent` | 1 byte | Semantic intent token (routes the packet) | | `0x05` | `TagThreatScore` | 2 bytes | Threat score (0--65535, big-endian uint16) | | `0x06` | `TagHMAC` | 32 bytes | HMAC-SHA256 signature | diff --git a/logging/compat.go b/logging/compat.go deleted file mode 100644 index 74f014b..0000000 --- a/logging/compat.go +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package logging - -// WithComponent returns a new Logger scoped to one component. -// Deprecated: Use ComponentLogger instead. -// -// transportLogger := logger.WithComponent("transport") -func (l *Logger) WithComponent(component string) *Logger { - return l.ComponentLogger(component) -} - -// Level returns the current log level. -// Deprecated: Use GetLevel instead. -// -// level := logger.Level() -func (l *Logger) Level() Level { - return l.GetLevel() -} - -// Global returns the global logger instance. -// Deprecated: Use GetGlobal instead. -// -// logger := Global() -func Global() *Logger { - return GetGlobal() -} diff --git a/logging/compat_test.go b/logging/compat_test.go deleted file mode 100644 index 1310c17..0000000 --- a/logging/compat_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package logging - -import ( - "bytes" - "testing" - - core "dappco.re/go/core" -) - -func TestGetLevel_Good(t *testing.T) { - logger := New(Config{Output: &bytes.Buffer{}, Level: LevelWarn}) - if logger.GetLevel() != LevelWarn { - t.Errorf("GetLevel() = %v, want %v", logger.GetLevel(), LevelWarn) - } - if logger.GetLevel() != logger.Level() { - t.Error("GetLevel() and Level() should return the same value") - } -} - -func TestWithComponent_Good(t *testing.T) { - var buf bytes.Buffer - parent := New(Config{Output: &buf, Level: LevelInfo}) - child := parent.WithComponent("TestChild") - child.Info("test message") - if !core.Contains(buf.String(), "[TestChild]") { - t.Error("WithComponent should set the component name") - } -} - -func TestGetGlobal_Good(t *testing.T) { - var buf bytes.Buffer - logger := New(Config{Output: &buf, Level: LevelInfo}) - SetGlobal(logger) - got := GetGlobal() - if got != Global() { - t.Error("GetGlobal() and Global() should return the same logger") - } - SetGlobal(New(DefaultConfig())) -} diff --git a/node/compat.go b/node/compat.go deleted file mode 100644 index fce12ad..0000000 --- a/node/compat.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides deprecated compatibility aliases and type aliases -// for spec compatibility. Canonical names are defined in their respective -// source files; these aliases exist so that code written against the spec -// documentation continues to compile. - -package node - -// NewNodeManagerWithPaths loads or creates a node identity store at explicit paths. -// Deprecated: Use NewNodeManagerFromPaths instead. -// -// nodeManager, err := NewNodeManagerWithPaths("/srv/p2p/private.key", "/srv/p2p/node.json") -func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error) { - return NewNodeManagerFromPaths(keyPath, configPath) -} - -// NewPeerRegistryWithPath loads or creates a peer registry at an explicit path. -// Deprecated: Use NewPeerRegistryFromPath instead. -// -// peerRegistry, err := NewPeerRegistryWithPath("/srv/p2p/peers.json") -func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error) { - return NewPeerRegistryFromPath(peersPath) -} - -// RegisterWithTransport installs the worker message handler on the transport. -// Deprecated: Use RegisterOnTransport instead. -// -// worker.RegisterWithTransport() -func (w *Worker) RegisterWithTransport() { - w.RegisterOnTransport() -} - -// ConnectedPeers returns the number of connected peers. -// Deprecated: Use ConnectedPeerCount instead. -// -// count := transport.ConnectedPeers() -func (t *Transport) ConnectedPeers() int { - return t.ConnectedPeerCount() -} - -// GetLogsPayload is an alias for LogsRequestPayload. -// Provided for spec compatibility. -// -// payload := GetLogsPayload{MinerName: "xmrig-0", Lines: 100} -type GetLogsPayload = LogsRequestPayload diff --git a/node/compat_test.go b/node/compat_test.go deleted file mode 100644 index 9b197aa..0000000 --- a/node/compat_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package node - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewNodeManagerWithPaths_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - - nm, err := NewNodeManagerWithPaths(keyPath, configPath) - require.NoError(t, err) - assert.NotNil(t, nm) - assert.False(t, nm.HasIdentity(), "fresh manager should have no identity") -} - -func TestNewPeerRegistryWithPath_Good(t *testing.T) { - peersPath := t.TempDir() + "/peers.json" - - pr, err := NewPeerRegistryWithPath(peersPath) - require.NoError(t, err) - assert.NotNil(t, pr) - assert.Equal(t, 0, pr.Count()) - pr.Close() -} - -func TestGetLogsPayload_Good(t *testing.T) { - var payload GetLogsPayload - payload.MinerName = "xmrig-0" - payload.Lines = 100 - payload.Since = 1234567890 - - var request LogsRequestPayload = payload - assert.Equal(t, "xmrig-0", request.MinerName) - assert.Equal(t, 100, request.Lines) - assert.Equal(t, int64(1234567890), request.Since) -} - -func TestTransportConnectedPeers_Good(t *testing.T) { - keyPath := t.TempDir() + "/private.key" - configPath := t.TempDir() + "/node.json" - peersPath := t.TempDir() + "/peers.json" - - nm, err := NewNodeManagerFromPaths(keyPath, configPath) - require.NoError(t, err) - - pr, err := NewPeerRegistryFromPath(peersPath) - require.NoError(t, err) - defer pr.Close() - - transport := NewTransport(nm, pr, DefaultTransportConfig()) - assert.Equal(t, transport.ConnectedPeerCount(), transport.ConnectedPeers()) - assert.Equal(t, 0, transport.ConnectedPeers()) -} diff --git a/node/levin/compat.go b/node/levin/compat.go deleted file mode 100644 index a88bdfb..0000000 --- a/node/levin/compat.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -// This file provides short-form aliases for Value constructors, -// matching the naming used in the spec documentation. - -package levin - -// Uint64Val creates a Value of TypeUint64. Short-form alias for Uint64Value. -// -// value := Uint64Val(42) -func Uint64Val(v uint64) Value { return Uint64Value(v) } - -// Uint32Val creates a Value of TypeUint32. Short-form alias for Uint32Value. -// -// value := Uint32Val(42) -func Uint32Val(v uint32) Value { return Uint32Value(v) } - -// Uint16Val creates a Value of TypeUint16. Short-form alias for Uint16Value. -// -// value := Uint16Val(42) -func Uint16Val(v uint16) Value { return Uint16Value(v) } - -// Uint8Val creates a Value of TypeUint8. Short-form alias for Uint8Value. -// -// value := Uint8Val(42) -func Uint8Val(v uint8) Value { return Uint8Value(v) } - -// Int64Val creates a Value of TypeInt64. Short-form alias for Int64Value. -// -// value := Int64Val(42) -func Int64Val(v int64) Value { return Int64Value(v) } - -// Int32Val creates a Value of TypeInt32. Short-form alias for Int32Value. -// -// value := Int32Val(42) -func Int32Val(v int32) Value { return Int32Value(v) } - -// Int16Val creates a Value of TypeInt16. Short-form alias for Int16Value. -// -// value := Int16Val(42) -func Int16Val(v int16) Value { return Int16Value(v) } - -// Int8Val creates a Value of TypeInt8. Short-form alias for Int8Value. -// -// value := Int8Val(42) -func Int8Val(v int8) Value { return Int8Value(v) } - -// BoolVal creates a Value of TypeBool. Short-form alias for BoolValue. -// -// value := BoolVal(true) -func BoolVal(v bool) Value { return BoolValue(v) } - -// DoubleVal creates a Value of TypeDouble. Short-form alias for DoubleValue. -// -// value := DoubleVal(3.14) -func DoubleVal(v float64) Value { return DoubleValue(v) } - -// StringVal creates a Value of TypeString. Short-form alias for StringValue. -// -// value := StringVal([]byte("hello")) -func StringVal(v []byte) Value { return StringValue(v) } - -// ObjectVal creates a Value of TypeObject. Short-form alias for ObjectValue. -// -// value := ObjectVal(Section{"id": StringVal([]byte("peer-1"))}) -func ObjectVal(s Section) Value { return ObjectValue(s) } - -// Uint64ArrayVal creates a typed array of uint64 values. Short-form alias for Uint64ArrayValue. -// -// value := Uint64ArrayVal([]uint64{1, 2, 3}) -func Uint64ArrayVal(vs []uint64) Value { return Uint64ArrayValue(vs) } - -// Uint32ArrayVal creates a typed array of uint32 values. Short-form alias for Uint32ArrayValue. -// -// value := Uint32ArrayVal([]uint32{1, 2, 3}) -func Uint32ArrayVal(vs []uint32) Value { return Uint32ArrayValue(vs) } - -// StringArrayVal creates a typed array of byte-string values. Short-form alias for StringArrayValue. -// -// value := StringArrayVal([][]byte{[]byte("a"), []byte("b")}) -func StringArrayVal(vs [][]byte) Value { return StringArrayValue(vs) } - -// ObjectArrayVal creates a typed array of Section values. Short-form alias for ObjectArrayValue. -// -// value := ObjectArrayVal([]Section{{"id": StringVal([]byte("peer-1"))}}) -func ObjectArrayVal(vs []Section) Value { return ObjectArrayValue(vs) } diff --git a/node/levin/compat_test.go b/node/levin/compat_test.go deleted file mode 100644 index 72de5f4..0000000 --- a/node/levin/compat_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2024-2026 Lethean Contributors -// SPDX-License-Identifier: EUPL-1.2 - -package levin - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestShortFormAliases_Good(t *testing.T) { - assert.Equal(t, Uint64Value(42), Uint64Val(42)) - assert.Equal(t, Uint32Value(42), Uint32Val(42)) - assert.Equal(t, Uint16Value(42), Uint16Val(42)) - assert.Equal(t, Uint8Value(42), Uint8Val(42)) - assert.Equal(t, Int64Value(-42), Int64Val(-42)) - assert.Equal(t, Int32Value(-42), Int32Val(-42)) - assert.Equal(t, Int16Value(-42), Int16Val(-42)) - assert.Equal(t, Int8Value(-42), Int8Val(-42)) - assert.Equal(t, BoolValue(true), BoolVal(true)) - assert.Equal(t, DoubleValue(3.14), DoubleVal(3.14)) - assert.Equal(t, StringValue([]byte("hello")), StringVal([]byte("hello"))) - - section := Section{"key": StringValue([]byte("value"))} - assert.Equal(t, ObjectValue(section), ObjectVal(section)) - - assert.Equal(t, Uint64ArrayValue([]uint64{1, 2}), Uint64ArrayVal([]uint64{1, 2})) - assert.Equal(t, Uint32ArrayValue([]uint32{1, 2}), Uint32ArrayVal([]uint32{1, 2})) - assert.Equal(t, StringArrayValue([][]byte{[]byte("a")}), StringArrayVal([][]byte{[]byte("a")})) - assert.Equal(t, ObjectArrayValue([]Section{section}), ObjectArrayVal([]Section{section})) -} - -func TestShortFormAliasesRoundTrip_Good(t *testing.T) { - s := Section{ - "u64": Uint64Val(0xCAFEBABE), - "str": StringVal([]byte("hello")), - "flag": BoolVal(true), - "obj": ObjectVal(Section{"nested": Int32Val(42)}), - } - - data, err := EncodeStorage(s) - require.NoError(t, err) - - decoded, err := DecodeStorage(data) - require.NoError(t, err) - - u64, err := decoded["u64"].AsUint64() - require.NoError(t, err) - assert.Equal(t, uint64(0xCAFEBABE), u64) - - str, err := decoded["str"].AsString() - require.NoError(t, err) - assert.Equal(t, []byte("hello"), str) - - flag, err := decoded["flag"].AsBool() - require.NoError(t, err) - assert.True(t, flag) - - obj, err := decoded["obj"].AsSection() - require.NoError(t, err) - nested, err := obj["nested"].AsInt32() - require.NoError(t, err) - assert.Equal(t, int32(42), nested) -} diff --git a/specs/logging.md b/specs/logging.md index 31de890..b3ba1ef 100644 --- a/specs/logging.md +++ b/specs/logging.md @@ -43,7 +43,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | | `DefaultConfig` | `func DefaultConfig() Config` | Returns the default configuration: stderr output, `LevelInfo`, and no component label. | -| `New` | `func New(cfg Config) *Logger` | Creates a `Logger` from `cfg`, substituting the default stderr writer when `cfg.Output` is `nil`. | +| `New` | `func New(config Config) *Logger` | Creates a `Logger` from `config`, substituting the default stderr writer when `config.Output` is `nil`. | | `SetGlobal` | `func SetGlobal(l *Logger)` | Replaces the package-level global logger instance. | | `GetGlobal` | `func GetGlobal() *Logger` | Returns the current package-level global logger. | | `SetGlobalLevel` | `func SetGlobalLevel(level Level)` | Updates the minimum severity on the current global logger. | @@ -67,8 +67,7 @@ Structured logger with configurable output, severity filtering, and component sc | Name | Signature | Description | | --- | --- | --- | -| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. Preferred over `WithComponent`. | -| `WithComponent` | `func (l *Logger) WithComponent(component string) *Logger` | Deprecated compatibility alias for `ComponentLogger`. | +| `ComponentLogger` | `func (l *Logger) ComponentLogger(component string) *Logger` | Returns a new logger scoped to `component`. | | `SetLevel` | `func (l *Logger) SetLevel(level Level)` | Sets the minimum severity that the logger will emit. | | `GetLevel` | `func (l *Logger) GetLevel() Level` | Returns the current minimum severity. | | `Debug` | `func (l *Logger) Debug(msg string, fields ...Fields)` | Logs `msg` at debug level after merging any supplied field maps. | diff --git a/specs/node-levin.md b/specs/node-levin.md index b5e8912..ca6b93c 100644 --- a/specs/node-levin.md +++ b/specs/node-levin.md @@ -48,7 +48,7 @@ type Value struct { } ``` -Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Val`, `StringVal`, and `ObjectArrayVal` create correctly-typed instances. +Tagged portable-storage value. The exported `Type` field identifies which internal scalar or array slot is populated; constructors such as `Uint64Value`, `StringValue`, and `ObjectArrayValue` create correctly-typed instances. ## Functions @@ -68,22 +68,22 @@ Tagged portable-storage value. The exported `Type` field identifies which intern | Name | Signature | Description | | --- | --- | --- | -| `Uint64Val` | `func Uint64Val(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | -| `Uint32Val` | `func Uint32Val(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | -| `Uint16Val` | `func Uint16Val(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | -| `Uint8Val` | `func Uint8Val(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | -| `Int64Val` | `func Int64Val(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | -| `Int32Val` | `func Int32Val(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | -| `Int16Val` | `func Int16Val(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | -| `Int8Val` | `func Int8Val(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | -| `BoolVal` | `func BoolVal(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | -| `DoubleVal` | `func DoubleVal(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | -| `StringVal` | `func StringVal(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | -| `ObjectVal` | `func ObjectVal(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | -| `Uint64ArrayVal` | `func Uint64ArrayVal(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | -| `Uint32ArrayVal` | `func Uint32ArrayVal(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | -| `StringArrayVal` | `func StringArrayVal(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | -| `ObjectArrayVal` | `func ObjectArrayVal(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | +| `Uint64Value` | `func Uint64Value(v uint64) Value` | Creates a scalar `Value` with `TypeUint64`. | +| `Uint32Value` | `func Uint32Value(v uint32) Value` | Creates a scalar `Value` with `TypeUint32`. | +| `Uint16Value` | `func Uint16Value(v uint16) Value` | Creates a scalar `Value` with `TypeUint16`. | +| `Uint8Value` | `func Uint8Value(v uint8) Value` | Creates a scalar `Value` with `TypeUint8`. | +| `Int64Value` | `func Int64Value(v int64) Value` | Creates a scalar `Value` with `TypeInt64`. | +| `Int32Value` | `func Int32Value(v int32) Value` | Creates a scalar `Value` with `TypeInt32`. | +| `Int16Value` | `func Int16Value(v int16) Value` | Creates a scalar `Value` with `TypeInt16`. | +| `Int8Value` | `func Int8Value(v int8) Value` | Creates a scalar `Value` with `TypeInt8`. | +| `BoolValue` | `func BoolValue(v bool) Value` | Creates a scalar `Value` with `TypeBool`. | +| `DoubleValue` | `func DoubleValue(v float64) Value` | Creates a scalar `Value` with `TypeDouble`. | +| `StringValue` | `func StringValue(v []byte) Value` | Creates a scalar `Value` with `TypeString`. The byte slice is stored without copying. | +| `ObjectValue` | `func ObjectValue(s Section) Value` | Creates a scalar `Value` with `TypeObject` that wraps a nested `Section`. | +| `Uint64ArrayValue` | `func Uint64ArrayValue(vs []uint64) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint64`. | +| `Uint32ArrayValue` | `func Uint32ArrayValue(vs []uint32) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeUint32`. | +| `StringArrayValue` | `func StringArrayValue(vs [][]byte) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeString`. | +| `ObjectArrayValue` | `func ObjectArrayValue(vs []Section) Value` | Creates an array `Value` tagged as `ArrayFlag | TypeObject`. | ### `*Connection` methods diff --git a/specs/node.md b/specs/node.md index 87324b3..99f6eb8 100644 --- a/specs/node.md +++ b/specs/node.md @@ -32,7 +32,7 @@ | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WSPath string; TLSCertPath string; TLSKeyPath string; MaxConns int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -43,7 +43,7 @@ | `DeployPayload` | `struct{ BundleType string; Data []byte; Checksum string; Name string }` | Deployment request carrying STIM-encrypted bundle bytes (or other bundle data), checksum, and logical name. | | `DisconnectPayload` | `struct{ Reason string; Code int }` | Disconnect notice with human-readable reason and optional disconnect code. | | `ErrorPayload` | `struct{ Code int; Message string; Details string }` | Payload used by `MsgError` responses. | -| `GetLogsPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | +| `LogsRequestPayload` | `struct{ MinerName string; Lines int; Since int64 }` | Request for miner console output, optionally bounded by line count and a Unix timestamp. | | `HandshakeAckPayload` | `struct{ Identity NodeIdentity; ChallengeResponse []byte; Accepted bool; Reason string }` | Handshake reply containing the responder identity, optional challenge response, acceptance flag, and optional rejection reason. | | `HandshakePayload` | `struct{ Identity NodeIdentity; Challenge []byte; Version string }` | Handshake request containing node identity, optional authentication challenge, and protocol version. | | `LogsPayload` | `struct{ MinerName string; Lines []string; HasMore bool }` | Returned miner log lines plus an indicator that more lines are available. | @@ -88,17 +88,15 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `:9091`, `/ws`, `MaxConns=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. | | `NewNodeManager` | `func NewNodeManager() (*NodeManager, error)` | Resolves XDG key and config paths, then loads an existing identity if present. | | `NewNodeManagerFromPaths` | `func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error)` | Creates a node manager from explicit key and config paths. | -| `NewNodeManagerWithPaths` | `func NewNodeManagerWithPaths(keyPath, configPath string) (*NodeManager, error)` | Deprecated compatibility alias for `NewNodeManagerFromPaths`. | | `NewPeerRateLimiter` | `func NewPeerRateLimiter(maxTokens, refillRate int) *PeerRateLimiter` | Creates a token bucket seeded with `maxTokens` and refilled at `refillRate` tokens per second. | | `NewPeerRegistry` | `func NewPeerRegistry() (*PeerRegistry, error)` | Resolves the XDG peers path, loads any persisted peers, and builds the selection KD-tree. | | `NewPeerRegistryFromPath` | `func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error)` | Creates a peer registry bound to `peersPath` with open authentication mode and an empty public-key allowlist. | -| `NewPeerRegistryWithPath` | `func NewPeerRegistryWithPath(peersPath string) (*PeerRegistry, error)` | Deprecated compatibility alias for `NewPeerRegistryFromPath`. | | `NewTransport` | `func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport` | Creates a transport with lifecycle context, a 5-minute message deduplicator, and a WebSocket upgrader that only accepts local origins. | | `NewWorker` | `func NewWorker(node *NodeManager, transport *Transport) *Worker` | Creates a worker, records its start time for uptime reporting, and defaults `DataDir` to `xdg.DataHome`. | @@ -213,7 +211,7 @@ | `Connections` | `func (t *Transport) Connections() iter.Seq[*PeerConnection]` | Returns an iterator over active peer connections. | | `Broadcast` | `func (t *Transport) Broadcast(msg *Message) error` | Sends `msg` to every connected peer except the sender identified by `msg.From`. | | `GetConnection` | `func (t *Transport) GetConnection(peerID string) *PeerConnection` | Returns the active connection for `peerID`, or `nil` when not connected. | -| `ConnectedPeers` | `func (t *Transport) ConnectedPeers() int` | Returns the number of active peer connections. | +| `ConnectedPeerCount` | `func (t *Transport) ConnectedPeerCount() int` | Returns the number of active peer connections. | ### `*PeerConnection` methods @@ -231,7 +229,6 @@ | `SetProfileManager` | `func (w *Worker) SetProfileManager(manager ProfileManager)` | Installs the profile manager used during deployment handling. | | `HandleMessage` | `func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message)` | Dispatches supported message types, sends normal replies on success, and emits `MsgError` responses when a handled command fails. | | `RegisterOnTransport` | `func (w *Worker) RegisterOnTransport()` | Registers `HandleMessage` as the transport's inbound message callback. | -| `RegisterWithTransport` | `func (w *Worker) RegisterWithTransport()` | Deprecated compatibility alias for `RegisterOnTransport`. | ### `*ProtocolError` methods diff --git a/ueps/compat.go b/ueps/compat.go deleted file mode 100644 index 2b81812..0000000 --- a/ueps/compat.go +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package ueps - -// Short-form tag aliases for spec compatibility. -const ( - // TagCurrentLay is a short-form alias for TagCurrentLayer. - TagCurrentLay = TagCurrentLayer - // TagTargetLay is a short-form alias for TagTargetLayer. - TagTargetLay = TagTargetLayer -) diff --git a/ueps/compat_test.go b/ueps/compat_test.go deleted file mode 100644 index d57fc01..0000000 --- a/ueps/compat_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package ueps - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTagAliases_Good(t *testing.T) { - assert.Equal(t, TagCurrentLayer, TagCurrentLay) - assert.Equal(t, TagTargetLayer, TagTargetLay) - assert.Equal(t, 0x02, TagCurrentLay) - assert.Equal(t, 0x03, TagTargetLay) -} From b5c7516224c4567f24114f1fb4d4fecfebb0ada3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:29:51 +0000 Subject: [PATCH 03/12] refactor(logging): remove stale alias wording Co-Authored-By: Virgil --- logging/logger.go | 9 +-------- logging/logger_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index 2746881..fed3fa9 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -27,7 +27,7 @@ const ( LevelError ) -// String returns the string representation of the log level. +// label := LevelWarn.String() func (l Level) String() string { switch l { case LevelDebug: @@ -133,7 +133,6 @@ func (stderrWriter) Write(p []byte) (int, error) { var defaultOutput io.Writer = stderrWriter{} -// log writes a log message at the specified level. func (l *Logger) log(level Level, message string, fields Fields) { l.mu.Lock() defer l.mu.Unlock() @@ -142,7 +141,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { return } - // Build the log line sb := core.NewBuilder() timestamp := time.Now().Format("2006/01/02 15:04:05") sb.WriteString(timestamp) @@ -159,7 +157,6 @@ func (l *Logger) log(level Level, message string, fields Fields) { sb.WriteString(" ") sb.WriteString(message) - // Add fields if present if len(fields) > 0 { sb.WriteString(" |") for k, v := range fields { @@ -226,8 +223,6 @@ func mergeFields(fields []Fields) Fields { return result } -// --- Global logger for convenience --- - var ( globalLogger = New(DefaultConfig()) globalMu sync.RWMutex @@ -260,8 +255,6 @@ func SetGlobalLevel(level Level) { globalLogger.SetLevel(level) } -// Global convenience functions that use the global logger - // Debug logs a debug message using the global logger. // // Debug("connected", Fields{"peer_id": "node-1"}) diff --git a/logging/logger_test.go b/logging/logger_test.go index b46aaec..c553130 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -101,15 +101,15 @@ func TestLogger_ComponentLogger_Good(t *testing.T) { child := parent.ComponentLogger("ChildComponent") child.Info("child message") - alias := parent.ComponentLogger("AliasComponent") - alias.Info("alias message") + secondaryLogger := parent.ComponentLogger("SecondaryComponent") + secondaryLogger.Info("secondary message") output := buf.String() if !core.Contains(output, "[ChildComponent]") { t.Error("Derived component name should appear") } - if !core.Contains(output, "[AliasComponent]") { - t.Error("Compatibility alias should preserve the component name") + if !core.Contains(output, "[SecondaryComponent]") { + t.Error("Secondary component should preserve the component name") } } From 8fc3be03a65036efb904759d5739beae68204404 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:36:45 +0000 Subject: [PATCH 04/12] refactor(node): align AX comments across public APIs Co-Authored-By: Virgil --- logging/logger.go | 84 ++++++++++------------------------------ node/controller.go | 4 +- node/dispatcher.go | 70 +++++++-------------------------- node/errors.go | 8 +--- node/identity.go | 6 +-- node/levin/connection.go | 36 +++++------------ node/message.go | 14 ++----- node/peer.go | 6 +-- node/protocol.go | 2 +- node/transport.go | 72 +++++++++------------------------- node/worker.go | 33 ++++------------ 11 files changed, 84 insertions(+), 251 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index fed3fa9..bbaac12 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -11,9 +11,7 @@ import ( core "dappco.re/go/core" ) -// Level represents the severity of a log message. -// -// level := LevelInfo +// level := LevelInfo type Level int const ( @@ -43,9 +41,7 @@ func (l Level) String() string { } } -// Logger provides structured logging with configurable output and level. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) type Logger struct { mu sync.RWMutex output io.Writer @@ -53,18 +49,14 @@ type Logger struct { component string } -// Config holds configuration for creating a new Logger. -// -// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} +// config := Config{Output: io.Discard, Level: LevelDebug, Component: "sync"} type Config struct { Output io.Writer Level Level Component string } -// DefaultConfig returns the default logger configuration. -// -// config := DefaultConfig() +// config := DefaultConfig() func DefaultConfig() Config { return Config{ Output: defaultOutput, @@ -73,9 +65,7 @@ func DefaultConfig() Config { } } -// New creates a logger from an explicit configuration. -// -// logger := New(DefaultConfig()) +// logger := New(DefaultConfig()) func New(config Config) *Logger { if config.Output == nil { config.Output = defaultOutput @@ -87,9 +77,7 @@ func New(config Config) *Logger { } } -// ComponentLogger returns a new Logger scoped to one component. -// -// transportLogger := logger.ComponentLogger("transport") +// transportLogger := logger.ComponentLogger("transport") func (l *Logger) ComponentLogger(component string) *Logger { return &Logger{ output: l.output, @@ -98,27 +86,21 @@ func (l *Logger) ComponentLogger(component string) *Logger { } } -// SetLevel changes the minimum log level. -// -// logger.SetLevel(LevelDebug) +// logger.SetLevel(LevelDebug) func (l *Logger) SetLevel(level Level) { l.mu.Lock() defer l.mu.Unlock() l.level = level } -// GetLevel returns the current log level. -// -// level := logger.GetLevel() +// level := logger.GetLevel() func (l *Logger) GetLevel() Level { l.mu.RLock() defer l.mu.RUnlock() return l.level } -// Fields represents key-value pairs for structured logging. -// -// fields := Fields{"peer_id": "node-1", "attempt": 2} +// fields := Fields{"peer_id": "node-1", "attempt": 2} type Fields map[string]any type stderrWriter struct{} @@ -228,92 +210,68 @@ var ( globalMu sync.RWMutex ) -// SetGlobal installs the global logger instance. -// -// SetGlobal(New(DefaultConfig())) +// SetGlobal(New(DefaultConfig())) func SetGlobal(l *Logger) { globalMu.Lock() defer globalMu.Unlock() globalLogger = l } -// GetGlobal returns the global logger instance. -// -// logger := GetGlobal() +// logger := GetGlobal() func GetGlobal() *Logger { globalMu.RLock() defer globalMu.RUnlock() return globalLogger } -// SetGlobalLevel changes the global logger level. -// -// SetGlobalLevel(LevelDebug) +// SetGlobalLevel(LevelDebug) func SetGlobalLevel(level Level) { globalMu.RLock() defer globalMu.RUnlock() globalLogger.SetLevel(level) } -// Debug logs a debug message using the global logger. -// -// Debug("connected", Fields{"peer_id": "node-1"}) +// Debug("connected", Fields{"peer_id": "node-1"}) func Debug(message string, fields ...Fields) { GetGlobal().Debug(message, fields...) } -// Info logs an informational message using the global logger. -// -// Info("worker started", Fields{"component": "transport"}) +// Info("worker started", Fields{"component": "transport"}) func Info(message string, fields ...Fields) { GetGlobal().Info(message, fields...) } -// Warn logs a warning message using the global logger. -// -// Warn("peer rate limited", Fields{"peer_id": "node-1"}) +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func Warn(message string, fields ...Fields) { GetGlobal().Warn(message, fields...) } -// Error logs an error message using the global logger. -// -// Error("send failed", Fields{"peer_id": "node-1"}) +// Error("send failed", Fields{"peer_id": "node-1"}) func Error(message string, fields ...Fields) { GetGlobal().Error(message, fields...) } -// Debugf logs a formatted debug message using the global logger. -// -// Debugf("connected peer %s", "node-1") +// Debugf("connected peer %s", "node-1") func Debugf(format string, args ...any) { GetGlobal().Debugf(format, args...) } -// Infof logs a formatted informational message using the global logger. -// -// Infof("worker %s ready", "node-1") +// Infof("worker %s ready", "node-1") func Infof(format string, args ...any) { GetGlobal().Infof(format, args...) } -// Warnf logs a formatted warning message using the global logger. -// -// Warnf("peer %s is slow", "node-1") +// Warnf("peer %s is slow", "node-1") func Warnf(format string, args ...any) { GetGlobal().Warnf(format, args...) } -// Errorf logs a formatted error message using the global logger. -// -// Errorf("peer %s failed", "node-1") +// Errorf("peer %s failed", "node-1") func Errorf(format string, args ...any) { GetGlobal().Errorf(format, args...) } -// ParseLevel parses a string into a log level. -// -// level, err := ParseLevel("warn") +// level, err := ParseLevel("warn") func ParseLevel(s string) (Level, error) { switch core.Upper(s) { case "DEBUG": diff --git a/node/controller.go b/node/controller.go index a683738..3d4ebc9 100644 --- a/node/controller.go +++ b/node/controller.go @@ -10,9 +10,7 @@ import ( "dappco.re/go/core/p2p/logging" ) -// Controller drives remote peer operations from a controller node. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) type Controller struct { nodeManager *NodeManager peerRegistry *PeerRegistry diff --git a/node/dispatcher.go b/node/dispatcher.go index 126e3b2..1307713 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -10,11 +10,7 @@ import ( "dappco.re/go/core/p2p/ueps" ) -// ThreatScoreThreshold is the maximum allowable threat score. Packets exceeding -// this value are silently dropped by the circuit breaker and logged as threat -// events. The threshold sits at ~76% of the uint16 range (50,000 / 65,535), -// providing headroom for legitimate elevated-risk traffic whilst rejecting -// clearly hostile payloads. +// threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 // Well-known intent identifiers. These correspond to the semantic tokens @@ -32,34 +28,16 @@ const ( // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error -// Dispatcher routes verified UEPS packets to registered intent handlers. -// It enforces a threat circuit breaker before routing: any packet whose -// ThreatScore exceeds ThreatScoreThreshold is dropped and logged. -// -// dispatcher := NewDispatcher() -// -// Design decisions: -// -// - Handlers are registered per IntentID (1:1 mapping). -// -// - Unknown intents are logged at WARN level and silently dropped (no error -// returned to the caller) to avoid back-pressure on the transport layer. -// -// - High-threat packets are dropped silently (logged at WARN) rather than -// returning an error, consistent with the "don't even parse the payload" -// philosophy from the original stub. -// -// - The dispatcher is safe for concurrent use; a RWMutex protects the -// handler map. +// dispatcher := NewDispatcher() +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) +// err := dispatcher.Dispatch(packet) type Dispatcher struct { handlers map[byte]IntentHandler mu sync.RWMutex log *logging.Logger } -// NewDispatcher creates a Dispatcher with no registered handlers. -// -// dispatcher := NewDispatcher() +// dispatcher := NewDispatcher() func NewDispatcher() *Dispatcher { return &Dispatcher{ handlers: make(map[byte]IntentHandler), @@ -70,12 +48,7 @@ func NewDispatcher() *Dispatcher { } } -// RegisterHandler associates an IntentHandler with a specific IntentID. -// Replacing an existing handler is allowed — the new handler takes effect immediately. -// -// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { -// return processComputeJob(packet.Payload) -// }) +// dispatcher.RegisterHandler(IntentCompute, func(packet *ueps.ParsedPacket) error { return nil }) func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { d.mu.Lock() defer d.mu.Unlock() @@ -85,11 +58,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// Handlers returns an iterator over all registered intent handlers. -// -// for intentID, handler := range dispatcher.Handlers() { -// log.Printf("registered intent 0x%02X", intentID) -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() @@ -103,17 +75,7 @@ func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { } } -// Dispatch routes a parsed UEPS packet through the threat circuit breaker -// and then to the appropriate intent handler. -// -// Behaviour: -// - Returns ErrorThreatScoreExceeded if the packet's ThreatScore exceeds the -// threshold (packet is dropped and logged). -// - Returns ErrorUnknownIntent if no handler is registered for the IntentID -// (packet is dropped and logged). -// - Returns nil on successful delivery to a handler, or any error the -// handler itself returns. -// - A nil packet returns ErrorNilPacket immediately. +// err := dispatcher.Dispatch(packet) func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { if packet == nil { return ErrorNilPacket @@ -146,17 +108,13 @@ func (d *Dispatcher) Dispatch(packet *ueps.ParsedPacket) error { return handler(packet) } -// Sentinel errors returned by Dispatch. var ( - // ErrorThreatScoreExceeded is returned when a packet's ThreatScore exceeds - // the safety threshold. + // err := ErrorThreatScoreExceeded ErrorThreatScoreExceeded = core.E("Dispatcher.Dispatch", core.Sprintf("packet rejected: threat score exceeds safety threshold (%d)", ThreatScoreThreshold), nil) - // ErrorUnknownIntent is returned when no handler is registered for the - // packet's IntentID. + // err := ErrorUnknownIntent ErrorUnknownIntent = core.E("Dispatcher.Dispatch", "packet dropped: unknown intent", nil) - // ErrorNilPacket is returned when a nil packet is passed to Dispatch. + // err := ErrorNilPacket ErrorNilPacket = core.E("Dispatcher.Dispatch", "nil packet", nil) ) - diff --git a/node/errors.go b/node/errors.go index f660be1..302a34a 100644 --- a/node/errors.go +++ b/node/errors.go @@ -2,14 +2,10 @@ package node import core "dappco.re/go/core" -// Shared error sentinels for the node package. var ( - // ErrorIdentityNotInitialized is returned when a node operation requires - // a node identity but none has been generated or loaded. + // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // ErrorMinerManagerNotConfigured is returned when a miner operation is - // attempted but no MinerManager has been set on the Worker. + // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) - diff --git a/node/identity.go b/node/identity.go index fc2aa2d..1309281 100644 --- a/node/identity.go +++ b/node/identity.go @@ -73,9 +73,7 @@ type NodeIdentity struct { Role NodeRole `json:"role"` } -// NodeManager handles node identity operations including key generation and storage. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() type NodeManager struct { identity *NodeIdentity privateKey []byte // Never serialized to JSON @@ -126,7 +124,7 @@ func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { return nm, nil } -// HasIdentity returns true if a node identity has been initialized. +// hasIdentity := nodeManager.HasIdentity() func (n *NodeManager) HasIdentity() bool { n.mu.RLock() defer n.mu.RUnlock() diff --git a/node/levin/connection.go b/node/levin/connection.go index 46670f2..74a3a04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -10,26 +10,22 @@ import ( "time" ) -// Levin protocol flags. +// flags := FlagRequest | FlagResponse const ( FlagRequest uint32 = 0x00000001 FlagResponse uint32 = 0x00000002 ) -// LevinProtocolVersion is the protocol version field written into every header. +// header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// Default timeout values for Connection read and write operations. +// conn.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// Connection wraps a net.Conn and provides framed Levin packet I/O. -// All writes are serialised by an internal mutex, making it safe to call -// WritePacket and WriteResponse concurrently from multiple goroutines. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -45,9 +41,7 @@ type Connection struct { writeMutex sync.Mutex } -// NewConnection creates a Connection that wraps conn with sensible defaults. -// -// connection := NewConnection(conn) +// conn := NewConnection(netConn) func NewConnection(conn net.Conn) *Connection { return &Connection{ MaxPayloadSize: MaxPayloadSize, @@ -57,9 +51,7 @@ func NewConnection(conn net.Conn) *Connection { } } -// WritePacket sends a Levin request or notification. -// -// err := conn.WritePacket(CommandPing, payload, true) +// err := conn.WritePacket(CommandPing, payload, true) func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, @@ -73,9 +65,7 @@ func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool return c.writeFrame(&header, payload) } -// WriteResponse sends a Levin response packet with the given return code. -// -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) +// err := conn.WriteResponse(CommandPing, payload, ReturnOK) func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, @@ -113,9 +103,7 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// ReadPacket reads and validates the next Levin packet. -// -// header, payload, err := conn.ReadPacket() +// header, payload, err := conn.ReadPacket() func (c *Connection) ReadPacket() (Header, []byte, error) { if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return Header{}, nil, err @@ -150,16 +138,12 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { return header, payload, nil } -// Close closes the underlying network connection. -// -// err := conn.Close() +// err := conn.Close() func (c *Connection) Close() error { return c.conn.Close() } -// RemoteAddr returns the remote address of the underlying connection as a string. -// -// addr := conn.RemoteAddr() +// addr := conn.RemoteAddr() func (c *Connection) RemoteAddr() string { return c.conn.RemoteAddr().String() } diff --git a/node/message.go b/node/message.go index 4931d8f..1640fde 100644 --- a/node/message.go +++ b/node/message.go @@ -126,9 +126,7 @@ func NewMessage(messageType MessageType, from, to string, payload any) (*Message }, nil } -// Reply creates a response message that points back to the original. -// -// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) +// reply, err := message.Reply(MessagePong, PongPayload{SentAt: 42, ReceivedAt: 43}) func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) { reply, err := NewMessage(messageType, m.To, m.From, payload) if err != nil { @@ -138,10 +136,8 @@ func (m *Message) Reply(messageType MessageType, payload any) (*Message, error) return reply, nil } -// ParsePayload decodes the payload into the supplied target. -// -// var ping PingPayload -// err := message.ParsePayload(&ping) +// var ping PingPayload +// err := message.ParsePayload(&ping) func (m *Message) ParsePayload(target any) error { if m.Payload == nil { return nil @@ -295,9 +291,7 @@ const ( ErrorCodeTimeout = 1005 ) -// NewErrorMessage builds an error response message for an existing request. -// -// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") +// errorMessage, err := NewErrorMessage("worker-1", "controller-1", ErrorCodeOperationFailed, "miner start failed", "req-1") func NewErrorMessage(from, to string, code int, message string, replyTo string) (*Message, error) { errorMessage, err := NewMessage(MessageError, from, to, ErrorPayload{ Code: code, diff --git a/node/peer.go b/node/peer.go index aeafe82..b44348a 100644 --- a/node/peer.go +++ b/node/peer.go @@ -53,7 +53,7 @@ const peerRegistrySaveDebounceInterval = 5 * time.Second type PeerAuthMode int const ( - // PeerAuthOpen allows any peer to connect (original behavior) + // PeerAuthOpen allows any peer to connect. PeerAuthOpen PeerAuthMode = iota // PeerAuthAllowlist only allows pre-registered peers or those with allowed public keys PeerAuthAllowlist @@ -98,9 +98,7 @@ func validatePeerName(name string) error { return nil } -// PeerRegistry manages known peers with KD-tree based selection. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() type PeerRegistry struct { peers map[string]*Peer kdTree *poindexter.KDTree[string] // KD-tree with peer ID as payload diff --git a/node/protocol.go b/node/protocol.go index 34f255d..72c1624 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -63,7 +63,7 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, return nil } -// DefaultResponseHandler is the default response handler instance. +// handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} // ValidateResponse is a convenience function using the default handler. diff --git a/node/transport.go b/node/transport.go index 4acdfff..9d9c989 100644 --- a/node/transport.go +++ b/node/transport.go @@ -144,9 +144,7 @@ func (d *MessageDeduplicator) Cleanup() { } } -// Transport manages WebSocket connections with SMSG encryption. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) type Transport struct { config TransportConfig httpServer *http.Server @@ -163,9 +161,7 @@ type Transport struct { waitGroup sync.WaitGroup } -// PeerRateLimiter implements a simple token bucket rate limiter per peer. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) type PeerRateLimiter struct { availableTokens int capacity int @@ -187,7 +183,7 @@ func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { } } -// Allow checks if a message is allowed and consumes a token if so +// allowed := rateLimiter.Allow() func (r *PeerRateLimiter) Allow() bool { r.mutex.Lock() defer r.mutex.Unlock() @@ -209,9 +205,7 @@ func (r *PeerRateLimiter) Allow() bool { return false } -// PeerConnection represents an active connection to a peer. -// -// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} +// peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { Peer *Peer Conn *websocket.Conn @@ -224,9 +218,7 @@ type PeerConnection struct { rateLimiter *PeerRateLimiter // Per-peer message rate limiting } -// NewTransport creates a WebSocket transport for a node and peer registry. -// -// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) +// transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) @@ -310,9 +302,7 @@ func (t *Transport) agentUserAgent() string { ) } -// Start opens the WebSocket listener and background maintenance loops. -// -// err := transport.Start() +// err := transport.Start() func (t *Transport) Start() error { mux := http.NewServeMux() mux.HandleFunc(t.config.webSocketPath(), t.handleWebSocketUpgrade) @@ -381,9 +371,7 @@ func (t *Transport) Start() error { return nil } -// Stop closes active connections and shuts the transport down cleanly. -// -// err := transport.Stop() +// err := transport.Stop() func (t *Transport) Stop() error { t.cancelLifecycle() @@ -410,18 +398,14 @@ func (t *Transport) Stop() error { return nil } -// OnMessage installs the handler for incoming messages before Start. -// -// transport.OnMessage(worker.HandleMessage) +// transport.OnMessage(worker.HandleMessage) func (t *Transport) OnMessage(handler MessageHandler) { t.mutex.Lock() defer t.mutex.Unlock() t.messageHandler = handler } -// Connect dials a peer, completes the handshake, and starts the session loops. -// -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -485,9 +469,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return pc, nil } -// Send transmits an encrypted message to a connected peer. -// -// err := transport.Send("worker-1", message) +// err := transport.Send("worker-1", message) func (t *Transport) Send(peerID string, msg *Message) error { t.mutex.RLock() pc, exists := t.connections[peerID] @@ -500,11 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// Connections returns an iterator over all active peer connections. -// -// for pc := range transport.Connections() { -// log.Printf("connected: %s", pc.Peer.ID) -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -518,10 +498,7 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } } -// Broadcast sends a message to every connected peer except the sender. -// The sender (msg.From) is excluded to prevent echo. -// -// err := transport.Broadcast(announcement) +// err := transport.Broadcast(announcement) func (t *Transport) Broadcast(msg *Message) error { conns := slices.Collect(t.Connections()) @@ -537,9 +514,7 @@ func (t *Transport) Broadcast(msg *Message) error { return lastErr } -// GetConnection returns an active connection to a peer. -// -// connection := transport.GetConnection("worker-1") +// connection := transport.GetConnection("worker-1") func (t *Transport) GetConnection(peerID string) *PeerConnection { t.mutex.RLock() defer t.mutex.RUnlock() @@ -973,9 +948,7 @@ func (t *Transport) removeConnection(pc *PeerConnection) { pc.Close() } -// Send sends an encrypted message over the connection. -// -// err := peerConnection.Send(message) +// err := peerConnection.Send(message) func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() @@ -993,9 +966,7 @@ func (pc *PeerConnection) Send(msg *Message) error { return pc.Conn.WriteMessage(websocket.BinaryMessage, data) } -// Close closes the connection. -// -// err := peerConnection.Close() +// err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { @@ -1021,9 +992,7 @@ const ( DisconnectShutdown = 1004 // Server shutdown ) -// GracefulClose sends a disconnect message before closing the connection. -// -// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) +// err := peerConnection.GracefulClose("server shutdown", DisconnectShutdown) func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { @@ -1090,12 +1059,9 @@ func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, return &msg, nil } -// ConnectedPeerCount returns the number of connected peers. -// -// count := transport.ConnectedPeerCount() +// count := transport.ConnectedPeerCount() func (t *Transport) ConnectedPeerCount() int { t.mutex.RLock() defer t.mutex.RUnlock() return len(t.connections) } - diff --git a/node/worker.go b/node/worker.go index 711c16b..3de7277 100644 --- a/node/worker.go +++ b/node/worker.go @@ -10,10 +10,7 @@ import ( "github.com/adrg/xdg" ) -// MinerManager interface for the mining package integration. -// This allows the node package to interact with mining.Manager without import cycles. -// -// var minerManager MinerManager +// var minerManager MinerManager type MinerManager interface { StartMiner(minerType string, config any) (MinerInstance, error) StopMiner(name string) error @@ -21,9 +18,7 @@ type MinerManager interface { GetMiner(name string) (MinerInstance, error) } -// MinerInstance represents a running miner for stats collection. -// -// var miner MinerInstance +// var miner MinerInstance type MinerInstance interface { GetName() string GetType() string @@ -31,17 +26,13 @@ type MinerInstance interface { GetConsoleHistory(lines int) []string } -// ProfileManager interface for profile operations. -// -// var profileManager ProfileManager +// var profileManager ProfileManager type ProfileManager interface { GetProfile(id string) (any, error) SaveProfile(profile any) error } -// Worker handles incoming messages on a worker node. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) type Worker struct { nodeManager *NodeManager transport *Transport @@ -51,9 +42,7 @@ type Worker struct { DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) } -// NewWorker creates a new Worker instance. -// -// worker := NewWorker(nodeManager, transport) +// worker := NewWorker(nodeManager, transport) func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { return &Worker{ nodeManager: nodeManager, @@ -63,23 +52,17 @@ func NewWorker(nodeManager *NodeManager, transport *Transport) *Worker { } } -// SetMinerManager attaches the miner manager used for miner operations. -// -// worker.SetMinerManager(minerManager) +// worker.SetMinerManager(minerManager) func (w *Worker) SetMinerManager(manager MinerManager) { w.minerManager = manager } -// SetProfileManager attaches the profile manager used for profile operations. -// -// worker.SetProfileManager(profileManager) +// worker.SetProfileManager(profileManager) func (w *Worker) SetProfileManager(manager ProfileManager) { w.profileManager = manager } -// HandleMessage routes an incoming message to the correct worker handler. -// -// worker.HandleMessage(peerConnection, message) +// worker.HandleMessage(peerConnection, message) func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) { var response *Message var err error From 4f4785505214245f81f8517c9cc8ab3a53cc06e7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:42:04 +0000 Subject: [PATCH 05/12] fix(transport): serialise graceful websocket close Co-Authored-By: Virgil --- node/transport.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/node/transport.go b/node/transport.go index 9d9c989..155d900 100644 --- a/node/transport.go +++ b/node/transport.go @@ -482,9 +482,9 @@ func (t *Transport) Send(peerID string, msg *Message) error { return pc.Send(msg) } -// for pc := range transport.Connections() { -// _ = pc -// } +// for pc := range transport.Connections() { +// _ = pc +// } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() @@ -953,6 +953,10 @@ func (pc *PeerConnection) Send(msg *Message) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + return pc.sendLocked(msg) +} + +func (pc *PeerConnection) sendLocked(msg *Message) error { data, err := pc.transport.encryptMessage(msg, pc.SharedSecret) if err != nil { return err @@ -996,10 +1000,10 @@ const ( func (pc *PeerConnection) GracefulClose(reason string, code int) error { var err error pc.closeOnce.Do(func() { + pc.writeMutex.Lock() + defer pc.writeMutex.Unlock() + // Try to send disconnect message (best effort). - // Note: we must NOT call SetWriteDeadline outside writeMutex — Send() - // already manages write deadlines under the lock. Setting it here - // without the lock races with concurrent Send() calls (P2P-RACE-1). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() if identity != nil { @@ -1009,7 +1013,7 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } msg, msgErr := NewMessage(MessageDisconnect, identity.ID, pc.Peer.ID, payload) if msgErr == nil { - pc.Send(msg) + _ = pc.sendLocked(msg) } } } From 643b93da01cb4190b1cfff81bb91a18449036784 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:48:17 +0000 Subject: [PATCH 06/12] refactor(p2p): align AX comments and harden UEPS parsing Co-Authored-By: Virgil --- logging/logger.go | 24 +++++++--------- ueps/packet.go | 54 +++++++++++------------------------- ueps/packet_coverage_test.go | 29 +++++++++++++++++++ ueps/reader.go | 37 ++++++++++++------------ 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/logging/logger.go b/logging/logger.go index bbaac12..0f463b7 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -1,4 +1,4 @@ -// Package logging provides structured logging with log levels and fields. +// logger := New(DefaultConfig()) package logging import ( @@ -15,13 +15,9 @@ import ( type Level int const ( - // LevelDebug is the most verbose log level. LevelDebug Level = iota - // LevelInfo is for general informational messages. LevelInfo - // LevelWarn is for warning messages. LevelWarn - // LevelError is for error messages. LevelError ) @@ -153,47 +149,47 @@ func (l *Logger) log(level Level, message string, fields Fields) { _, _ = l.output.Write([]byte(sb.String())) } -// Debug logs a debug message. +// Debug("connected", Fields{"peer_id": "node-1"}) func (l *Logger) Debug(message string, fields ...Fields) { l.log(LevelDebug, message, mergeFields(fields)) } -// Info logs an informational message. +// Info("worker started", Fields{"component": "transport"}) func (l *Logger) Info(message string, fields ...Fields) { l.log(LevelInfo, message, mergeFields(fields)) } -// Warn logs a warning message. +// Warn("peer rate limited", Fields{"peer_id": "node-1"}) func (l *Logger) Warn(message string, fields ...Fields) { l.log(LevelWarn, message, mergeFields(fields)) } -// Error logs an error message. +// Error("send failed", Fields{"peer_id": "node-1"}) func (l *Logger) Error(message string, fields ...Fields) { l.log(LevelError, message, mergeFields(fields)) } -// Debugf logs a formatted debug message. +// Debugf("connected peer %s", "node-1") func (l *Logger) Debugf(format string, args ...any) { l.log(LevelDebug, core.Sprintf(format, args...), nil) } -// Infof logs a formatted informational message. +// Infof("worker %s ready", "node-1") func (l *Logger) Infof(format string, args ...any) { l.log(LevelInfo, core.Sprintf(format, args...), nil) } -// Warnf logs a formatted warning message. +// Warnf("peer %s is slow", "node-1") func (l *Logger) Warnf(format string, args ...any) { l.log(LevelWarn, core.Sprintf(format, args...), nil) } -// Errorf logs a formatted error message. +// Errorf("peer %s failed", "node-1") func (l *Logger) Errorf(format string, args ...any) { l.log(LevelError, core.Sprintf(format, args...), nil) } -// mergeFields combines multiple Fields maps into one. +// fields := mergeFields([]Fields{{"peer_id": "node-1"}, {"attempt": 2}}) func mergeFields(fields []Fields) Fields { if len(fields) == 0 { return nil diff --git a/ueps/packet.go b/ueps/packet.go index 45ed113..f65a7b4 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -10,60 +10,49 @@ import ( core "dappco.re/go/core" ) -// TLV Types const ( TagVersion = 0x01 TagCurrentLayer = 0x02 TagTargetLayer = 0x03 TagIntent = 0x04 TagThreatScore = 0x05 - TagHMAC = 0x06 // The Signature - TagPayload = 0xFF // The Data + TagHMAC = 0x06 + TagPayload = 0xFF ) -// UEPSHeader represents the conscious routing metadata. -// -// header := UEPSHeader{IntentID: 0x01} +// header := UEPSHeader{Version: 0x09, CurrentLayer: 5, TargetLayer: 5, IntentID: 0x01} type UEPSHeader struct { - Version uint8 // Default 0x09 + Version uint8 CurrentLayer uint8 TargetLayer uint8 - IntentID uint8 // Semantic Token - ThreatScore uint16 // 0-65535 + IntentID uint8 + ThreatScore uint16 } -// PacketBuilder builds a signed UEPS frame from a concrete intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) type PacketBuilder struct { Header UEPSHeader Payload []byte } -// NewBuilder creates a packet builder for a specific intent and payload. -// -// builder := NewBuilder(0x20, []byte("hello")) +// builder := NewBuilder(0x20, []byte("hello")) func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { return &PacketBuilder{ Header: UEPSHeader{ - Version: 0x09, // IPv9 - CurrentLayer: 5, // Application - TargetLayer: 5, // Application + Version: 0x09, + CurrentLayer: 5, + TargetLayer: 5, IntentID: intentID, - ThreatScore: 0, // Assumed innocent until proven guilty + ThreatScore: 0, }, Payload: payload, } } -// MarshalAndSign signs a packet with a shared secret. -// -// frame, err := builder.MarshalAndSign(sharedSecret) +// frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { buf := new(bytes.Buffer) - // 1. Write Standard Header Tags (0x01 - 0x05) - // We write these first because they are part of what we sign. if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } @@ -77,30 +66,21 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return nil, err } - // Threat Score is uint16, needs binary packing tsBuf := make([]byte, 2) binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { return nil, err } - // 2. Calculate HMAC - // The signature covers: Existing Header TLVs + The Payload - // It does NOT cover the HMAC TLV tag itself (obviously) mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) // The headers so far - mac.Write(p.Payload) // The data + mac.Write(buf.Bytes()) + mac.Write(p.Payload) signature := mac.Sum(nil) - // 3. Write HMAC TLV (0x06) - // Length is 32 bytes for SHA256 if err := writeTLV(buf, TagHMAC, signature); err != nil { return nil, err } - // 4. Write Payload TLV (0xFF) - // Fixed: Now uses writeTLV which provides a 2-byte length prefix. - // This prevents the io.ReadAll DoS and allows multiple packets in a stream. if err := writeTLV(buf, TagPayload, p.Payload); err != nil { return nil, err } @@ -108,10 +88,8 @@ func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { return buf.Bytes(), nil } -// writeTLV writes a single tag-length-value record with a 2-byte length prefix. -// It supports payloads up to 64KB. +// writeTLV(&buf, TagPayload, []byte("hello")) func writeTLV(w io.Writer, tag uint8, value []byte) error { - // Check length constraint (2 byte length = max 65535 bytes) if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } diff --git a/ueps/packet_coverage_test.go b/ueps/packet_coverage_test.go index 6e3eced..ffd2572 100644 --- a/ueps/packet_coverage_test.go +++ b/ueps/packet_coverage_test.go @@ -211,3 +211,32 @@ func TestPacketCoverage_ReadAndVerify_ManualPacket_PayloadReadError_Bad(t *testi require.Error(t, err) assert.Equal(t, io.ErrUnexpectedEOF, err) } + +// TestReadAndVerify_MalformedHeaderTLV_Bad verifies malformed header values +// return an error instead of panicking during TLV reconstruction. +func TestPacketCoverage_ReadAndVerify_MalformedHeaderTLV_Bad(t *testing.T) { + tests := []struct { + name string + frame []byte + wantErr string + }{ + { + name: "ZeroLengthVersion", + frame: []byte{TagVersion, 0x00, 0x00}, + wantErr: "malformed version TLV", + }, + { + name: "ShortThreatScore", + frame: []byte{TagThreatScore, 0x00, 0x01, 0xFF}, + wantErr: "malformed threat score TLV", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(tc.frame)), testSecret) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/ueps/reader.go b/ueps/reader.go index e620ed1..a024c9c 100644 --- a/ueps/reader.go +++ b/ueps/reader.go @@ -11,84 +11,83 @@ import ( core "dappco.re/go/core" ) -// ParsedPacket holds the verified data -// -// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} +// packet := &ParsedPacket{Header: UEPSHeader{IntentID: 0x01}} type ParsedPacket struct { Header UEPSHeader Payload []byte } -// ReadAndVerify reads a UEPS frame from the stream and validates the HMAC. -// It consumes the stream up to the end of the packet. -// -// packet, err := ReadAndVerify(reader, sharedSecret) +// packet, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(frame)), sharedSecret) func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) { - // Buffer to reconstruct the data for HMAC verification var signedData bytes.Buffer header := UEPSHeader{} var signature []byte var payload []byte - // Loop through TLVs for { - // 1. Read Tag tag, err := r.ReadByte() if err != nil { return nil, err } - // 2. Read Length (2-byte big-endian uint16) lenBuf := make([]byte, 2) if _, err := io.ReadFull(r, lenBuf); err != nil { return nil, err } length := int(binary.BigEndian.Uint16(lenBuf)) - // 3. Read Value value := make([]byte, length) if _, err := io.ReadFull(r, value); err != nil { return nil, err } - // 4. Handle Tag switch tag { case TagVersion: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed version TLV", nil) + } header.Version = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagCurrentLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed current layer TLV", nil) + } header.CurrentLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagTargetLayer: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed target layer TLV", nil) + } header.TargetLayer = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagIntent: + if len(value) != 1 { + return nil, core.E("ueps.ReadAndVerify", "malformed intent TLV", nil) + } header.IntentID = value[0] signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagThreatScore: + if len(value) != 2 { + return nil, core.E("ueps.ReadAndVerify", "malformed threat score TLV", nil) + } header.ThreatScore = binary.BigEndian.Uint16(value) signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) case TagHMAC: signature = value - // HMAC tag itself is not part of the signed data case TagPayload: payload = value - // Exit loop after payload (last tag in UEPS frame) - // Note: The HMAC covers the Payload but NOT the TagPayload/Length bytes - // to match the PacketBuilder.MarshalAndSign logic. goto verify default: - // Unknown tag (future proofing), verify it but ignore semantics signedData.WriteByte(tag) signedData.Write(lenBuf) signedData.Write(value) @@ -100,8 +99,6 @@ verify: return nil, core.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil) } - // 5. Verify HMAC - // Reconstruct: Headers (signedData) + Payload mac := hmac.New(sha256.New, sharedSecret) mac.Write(signedData.Bytes()) mac.Write(payload) From 723f71143e933933d5af4b10e02ba8617c490492 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 13:56:56 +0000 Subject: [PATCH 07/12] refactor(node): align AX naming across transport and protocol helpers Co-Authored-By: Virgil --- node/buffer_pool.go | 18 +++++----- node/levin/connection.go | 74 ++++++++++++++++++++-------------------- node/levin/header.go | 52 ++++++++++++++-------------- node/levin/storage.go | 56 +++++++++++++++--------------- node/levin/varint.go | 60 ++++++++++++++++---------------- node/transport.go | 60 ++++++++++++++++---------------- ueps/packet.go | 34 +++++++++--------- 7 files changed, 177 insertions(+), 177 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index ace5915..91f160d 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -17,16 +17,16 @@ var bufferPool = sync.Pool{ // getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { - buf := bufferPool.Get().(*bytes.Buffer) - buf.Reset() - return buf + buffer := bufferPool.Get().(*bytes.Buffer) + buffer.Reset() + return buffer } // putBuffer returns a buffer to the pool. -func putBuffer(buf *bytes.Buffer) { +func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) - if buf.Cap() <= 65536 { - bufferPool.Put(buf) + if buffer.Cap() <= 65536 { + bufferPool.Put(buffer) } } @@ -34,9 +34,9 @@ func putBuffer(buf *bytes.Buffer) { // restores the historical no-EscapeHTML behaviour expected by the node package. // Returns a copy of the encoded bytes (safe to use after the function returns). // -// data, err := MarshalJSON(v) -func MarshalJSON(v any) ([]byte, error) { - encoded := core.JSONMarshal(v) +// data, err := MarshalJSON(value) +func MarshalJSON(value any) ([]byte, error) { + encoded := core.JSONMarshal(value) if !encoded.OK { return nil, encoded.Value.(error) } diff --git a/node/levin/connection.go b/node/levin/connection.go index 74a3a04..76bd643 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -19,13 +19,13 @@ const ( // header.ProtocolVersion = LevinProtocolVersion const LevinProtocolVersion uint32 = 1 -// conn.ReadTimeout = DefaultReadTimeout +// connection.ReadTimeout = DefaultReadTimeout const ( DefaultReadTimeout = 120 * time.Second DefaultWriteTimeout = 30 * time.Second ) -// conn := NewConnection(netConn) +// connection := NewConnection(networkConnection) type Connection struct { // MaxPayloadSize is the upper bound accepted for incoming payloads. // Defaults to the package-level MaxPayloadSize (100 MB). @@ -37,65 +37,65 @@ type Connection struct { // WriteTimeout is the deadline applied before each write call. WriteTimeout time.Duration - conn net.Conn - writeMutex sync.Mutex + networkConnection net.Conn + writeMutex sync.Mutex } -// conn := NewConnection(netConn) -func NewConnection(conn net.Conn) *Connection { +// connection := NewConnection(networkConnection) +func NewConnection(connection net.Conn) *Connection { return &Connection{ - MaxPayloadSize: MaxPayloadSize, - ReadTimeout: DefaultReadTimeout, - WriteTimeout: DefaultWriteTimeout, - conn: conn, + MaxPayloadSize: MaxPayloadSize, + ReadTimeout: DefaultReadTimeout, + WriteTimeout: DefaultWriteTimeout, + networkConnection: connection, } } -// err := conn.WritePacket(CommandPing, payload, true) -func (c *Connection) WritePacket(cmd uint32, payload []byte, expectResponse bool) error { +// err := connection.WritePacket(CommandPing, payload, true) +func (connection *Connection) WritePacket(commandID uint32, payload []byte, expectResponse bool) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: expectResponse, - Command: cmd, + Command: commandID, ReturnCode: ReturnOK, Flags: FlagRequest, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } -// err := conn.WriteResponse(CommandPing, payload, ReturnOK) -func (c *Connection) WriteResponse(cmd uint32, payload []byte, returnCode int32) error { +// err := connection.WriteResponse(CommandPing, payload, ReturnOK) +func (connection *Connection) WriteResponse(commandID uint32, payload []byte, returnCode int32) error { header := Header{ Signature: Signature, PayloadSize: uint64(len(payload)), ExpectResponse: false, - Command: cmd, + Command: commandID, ReturnCode: returnCode, Flags: FlagResponse, ProtocolVersion: LevinProtocolVersion, } - return c.writeFrame(&header, payload) + return connection.writeFrame(&header, payload) } // writeFrame serialises header + payload and writes them atomically. -func (c *Connection) writeFrame(header *Header, payload []byte) error { - buf := EncodeHeader(header) +func (connection *Connection) writeFrame(header *Header, payload []byte) error { + headerBytes := EncodeHeader(header) - c.writeMutex.Lock() - defer c.writeMutex.Unlock() + connection.writeMutex.Lock() + defer connection.writeMutex.Unlock() - if err := c.conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { + if err := connection.networkConnection.SetWriteDeadline(time.Now().Add(connection.WriteTimeout)); err != nil { return err } - if _, err := c.conn.Write(buf[:]); err != nil { + if _, err := connection.networkConnection.Write(headerBytes[:]); err != nil { return err } if len(payload) > 0 { - if _, err := c.conn.Write(payload); err != nil { + if _, err := connection.networkConnection.Write(payload); err != nil { return err } } @@ -103,15 +103,15 @@ func (c *Connection) writeFrame(header *Header, payload []byte) error { return nil } -// header, payload, err := conn.ReadPacket() -func (c *Connection) ReadPacket() (Header, []byte, error) { - if err := c.conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { +// header, payload, err := connection.ReadPacket() +func (connection *Connection) ReadPacket() (Header, []byte, error) { + if err := connection.networkConnection.SetReadDeadline(time.Now().Add(connection.ReadTimeout)); err != nil { return Header{}, nil, err } // Read header. var headerBytes [HeaderSize]byte - if _, err := io.ReadFull(c.conn, headerBytes[:]); err != nil { + if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err } @@ -121,7 +121,7 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } // Check against the connection-specific payload limit. - if header.PayloadSize > c.MaxPayloadSize { + if header.PayloadSize > connection.MaxPayloadSize { return Header{}, nil, ErrorPayloadTooBig } @@ -131,19 +131,19 @@ func (c *Connection) ReadPacket() (Header, []byte, error) { } payload := make([]byte, header.PayloadSize) - if _, err := io.ReadFull(c.conn, payload); err != nil { + if _, err := io.ReadFull(connection.networkConnection, payload); err != nil { return Header{}, nil, err } return header, payload, nil } -// err := conn.Close() -func (c *Connection) Close() error { - return c.conn.Close() +// err := connection.Close() +func (connection *Connection) Close() error { + return connection.networkConnection.Close() } -// addr := conn.RemoteAddr() -func (c *Connection) RemoteAddr() string { - return c.conn.RemoteAddr().String() +// addr := connection.RemoteAddr() +func (connection *Connection) RemoteAddr() string { + return connection.networkConnection.RemoteAddr().String() } diff --git a/node/levin/header.go b/node/levin/header.go index 2f69577..ee4d9ab 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -60,43 +60,43 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises h into a fixed-size 33-byte array (little-endian). +// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). // // encoded := EncodeHeader(header) -func EncodeHeader(h *Header) [HeaderSize]byte { - var buf [HeaderSize]byte - binary.LittleEndian.PutUint64(buf[0:8], h.Signature) - binary.LittleEndian.PutUint64(buf[8:16], h.PayloadSize) - if h.ExpectResponse { - buf[16] = 0x01 +func EncodeHeader(header *Header) [HeaderSize]byte { + var headerBytes [HeaderSize]byte + binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) + binary.LittleEndian.PutUint64(headerBytes[8:16], header.PayloadSize) + if header.ExpectResponse { + headerBytes[16] = 0x01 } else { - buf[16] = 0x00 + headerBytes[16] = 0x00 } - binary.LittleEndian.PutUint32(buf[17:21], h.Command) - binary.LittleEndian.PutUint32(buf[21:25], uint32(h.ReturnCode)) - binary.LittleEndian.PutUint32(buf[25:29], h.Flags) - binary.LittleEndian.PutUint32(buf[29:33], h.ProtocolVersion) - return buf + binary.LittleEndian.PutUint32(headerBytes[17:21], header.Command) + binary.LittleEndian.PutUint32(headerBytes[21:25], uint32(header.ReturnCode)) + binary.LittleEndian.PutUint32(headerBytes[25:29], header.Flags) + binary.LittleEndian.PutUint32(headerBytes[29:33], header.ProtocolVersion) + return headerBytes } // DecodeHeader deserialises a 33-byte array into a Header, validating // the magic signature. // -// header, err := DecodeHeader(buf) -func DecodeHeader(buf [HeaderSize]byte) (Header, error) { - var h Header - h.Signature = binary.LittleEndian.Uint64(buf[0:8]) - if h.Signature != Signature { +// header, err := DecodeHeader(headerBytes) +func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { + var header Header + header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) + if header.Signature != Signature { return Header{}, ErrorBadSignature } - h.PayloadSize = binary.LittleEndian.Uint64(buf[8:16]) - if h.PayloadSize > MaxPayloadSize { + header.PayloadSize = binary.LittleEndian.Uint64(headerBytes[8:16]) + if header.PayloadSize > MaxPayloadSize { return Header{}, ErrorPayloadTooBig } - h.ExpectResponse = buf[16] == 0x01 - h.Command = binary.LittleEndian.Uint32(buf[17:21]) - h.ReturnCode = int32(binary.LittleEndian.Uint32(buf[21:25])) - h.Flags = binary.LittleEndian.Uint32(buf[25:29]) - h.ProtocolVersion = binary.LittleEndian.Uint32(buf[29:33]) - return h, nil + header.ExpectResponse = headerBytes[16] == 0x01 + header.Command = binary.LittleEndian.Uint32(headerBytes[17:21]) + header.ReturnCode = int32(binary.LittleEndian.Uint32(headerBytes[21:25])) + header.Flags = binary.LittleEndian.Uint32(headerBytes[25:29]) + header.ProtocolVersion = binary.LittleEndian.Uint32(headerBytes[29:33]) + return header, nil } diff --git a/node/levin/storage.go b/node/levin/storage.go index a6c74e2..9a6d4ed 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -83,62 +83,62 @@ type Value struct { // Uint64Value creates a Value of TypeUint64. // // value := Uint64Value(42) -func Uint64Value(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} } +func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } // Uint32Value creates a Value of TypeUint32. // // value := Uint32Value(42) -func Uint32Value(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} } +func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } // Uint16Value creates a Value of TypeUint16. // // value := Uint16Value(42) -func Uint16Value(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} } +func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } // Uint8Value creates a Value of TypeUint8. // // value := Uint8Value(42) -func Uint8Value(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} } +func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } // Int64Value creates a Value of TypeInt64. // // value := Int64Value(42) -func Int64Value(v int64) Value { return Value{Type: TypeInt64, intVal: v} } +func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } // Int32Value creates a Value of TypeInt32. // // value := Int32Value(42) -func Int32Value(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} } +func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } // Int16Value creates a Value of TypeInt16. // // value := Int16Value(42) -func Int16Value(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} } +func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } // Int8Value creates a Value of TypeInt8. // // value := Int8Value(42) -func Int8Value(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} } +func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } // BoolValue creates a Value of TypeBool. // // value := BoolValue(true) -func BoolValue(v bool) Value { return Value{Type: TypeBool, boolVal: v} } +func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } // DoubleValue creates a Value of TypeDouble. // // value := DoubleValue(3.14) -func DoubleValue(v float64) Value { return Value{Type: TypeDouble, floatVal: v} } +func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } // StringValue creates a Value of TypeString. The slice is not copied. // // value := StringValue([]byte("hello")) -func StringValue(v []byte) Value { return Value{Type: TypeString, bytesVal: v} } +func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } // ObjectValue creates a Value of TypeObject wrapping a nested Section. // // value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) -func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} } +func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors @@ -147,29 +147,29 @@ func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} // Uint64ArrayValue creates a typed array of uint64 values. // // value := Uint64ArrayValue([]uint64{1, 2, 3}) -func Uint64ArrayValue(vs []uint64) Value { - return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs} +func Uint64ArrayValue(values []uint64) Value { + return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } // Uint32ArrayValue creates a typed array of uint32 values. // // value := Uint32ArrayValue([]uint32{1, 2, 3}) -func Uint32ArrayValue(vs []uint32) Value { - return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs} +func Uint32ArrayValue(values []uint32) Value { + return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } // StringArrayValue creates a typed array of byte-string values. // // value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) -func StringArrayValue(vs [][]byte) Value { - return Value{Type: ArrayFlag | TypeString, stringArray: vs} +func StringArrayValue(values [][]byte) Value { + return Value{Type: ArrayFlag | TypeString, stringArray: values} } // ObjectArrayValue creates a typed array of Section values. // // value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) -func ObjectArrayValue(vs []Section) Value { - return Value{Type: ArrayFlag | TypeObject, objectArray: vs} +func ObjectArrayValue(values []Section) Value { + return Value{Type: ArrayFlag | TypeObject, objectArray: values} } // --------------------------------------------------------------------------- @@ -317,25 +317,25 @@ func (v Value) AsSectionArray() ([]Section, error) { // deterministic output. // // data, err := EncodeStorage(section) -func EncodeStorage(s Section) ([]byte, error) { - buf := make([]byte, 0, 256) +func EncodeStorage(section Section) ([]byte, error) { + buffer := make([]byte, 0, 256) // 9-byte storage header. var hdr [StorageHeaderSize]byte binary.LittleEndian.PutUint32(hdr[0:4], StorageSignatureA) binary.LittleEndian.PutUint32(hdr[4:8], StorageSignatureB) hdr[8] = StorageVersion - buf = append(buf, hdr[:]...) + buffer = append(buffer, hdr[:]...) // Encode root section. - out, err := encodeSection(buf, s) + out, err := encodeSection(buffer, section) if err != nil { return nil, err } return out, nil } -// encodeSection appends a section (entry count + entries) to buf. +// encodeSection appends a section (entry count + entries) to buffer. func encodeSection(buf []byte, s Section) ([]byte, error) { // Sort keys for deterministic output. keys := slices.Sorted(maps.Keys(s)) @@ -510,7 +510,7 @@ func DecodeStorage(data []byte) (Section, error) { return s, err } -// decodeSection reads a section from buf and returns the section plus +// decodeSection reads a section from buffer and returns the section plus // the number of bytes consumed. func decodeSection(buf []byte) (Section, int, error) { count, n, err := UnpackVarint(buf) @@ -556,7 +556,7 @@ func decodeSection(buf []byte) (Section, int, error) { return s, off, nil } -// decodeValue reads a value of the given type tag from buf and returns +// decodeValue reads a value of the given type tag from buffer and returns // the value plus bytes consumed. func decodeValue(buf []byte, tag uint8) (Value, int, error) { // Array types. @@ -656,7 +656,7 @@ func decodeValue(buf []byte, tag uint8) (Value, int, error) { } } -// decodeArray reads a typed array from buf (tag has ArrayFlag set). +// decodeArray reads a typed array from buffer (tag has ArrayFlag set). func decodeArray(buf []byte, tag uint8) (Value, int, error) { elemType := tag & ^ArrayFlag diff --git a/node/levin/varint.go b/node/levin/varint.go index 5160646..2d26512 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,67 +28,67 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes v using the epee portable-storage varint scheme. +// PackVarint encodes value using the epee portable-storage varint scheme. // The low two bits of the first byte indicate the total encoded width; // the remaining bits carry the value in little-endian order. // // encoded := PackVarint(42) -func PackVarint(v uint64) []byte { +func PackVarint(value uint64) []byte { switch { - case v <= varintMax1: - return []byte{byte((v << 2) | varintMark1)} - case v <= varintMax2: - raw := uint16((v << 2) | varintMark2) - buf := make([]byte, 2) - binary.LittleEndian.PutUint16(buf, raw) - return buf - case v <= varintMax4: - raw := uint32((v << 2) | varintMark4) - buf := make([]byte, 4) - binary.LittleEndian.PutUint32(buf, raw) - return buf + case value <= varintMax1: + return []byte{byte((value << 2) | varintMark1)} + case value <= varintMax2: + raw := uint16((value << 2) | varintMark2) + buffer := make([]byte, 2) + binary.LittleEndian.PutUint16(buffer, raw) + return buffer + case value <= varintMax4: + raw := uint32((value << 2) | varintMark4) + buffer := make([]byte, 4) + binary.LittleEndian.PutUint32(buffer, raw) + return buffer default: - raw := (v << 2) | varintMark8 - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, raw) - return buf + raw := (value << 2) | varintMark8 + buffer := make([]byte, 8) + binary.LittleEndian.PutUint64(buffer, raw) + return buffer } } -// UnpackVarint decodes one epee portable-storage varint from buf. +// UnpackVarint decodes one epee portable-storage varint from buffer. // It returns the decoded value, the number of bytes consumed, and any error. // -// value, err := UnpackVarint(data) -func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { - if len(buf) == 0 { +// value, err := UnpackVarint(buffer) +func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { + if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated } - mark := buf[0] & varintMask + mark := buffer[0] & varintMask switch mark { case varintMark1: - value = uint64(buf[0]) >> 2 + value = uint64(buffer[0]) >> 2 return value, 1, nil case varintMark2: - if len(buf) < 2 { + if len(buffer) < 2 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint16(buf[:2]) + raw := binary.LittleEndian.Uint16(buffer[:2]) value = uint64(raw) >> 2 return value, 2, nil case varintMark4: - if len(buf) < 4 { + if len(buffer) < 4 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint32(buf[:4]) + raw := binary.LittleEndian.Uint32(buffer[:4]) value = uint64(raw) >> 2 return value, 4, nil case varintMark8: - if len(buf) < 8 { + if len(buffer) < 8 { return 0, 0, ErrorVarintTruncated } - raw := binary.LittleEndian.Uint64(buf[:8]) + raw := binary.LittleEndian.Uint64(buffer[:8]) value = raw >> 2 return value, 8, nil default: diff --git a/node/transport.go b/node/transport.go index 155d900..f580073 100644 --- a/node/transport.go +++ b/node/transport.go @@ -41,7 +41,7 @@ const ( // TransportConfig configures the WebSocket transport. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -55,7 +55,7 @@ type TransportConfig struct { // DefaultTransportConfig returns sensible defaults. // -// cfg := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -219,13 +219,13 @@ type PeerConnection struct { } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) -func NewTransport(node *NodeManager, registry *PeerRegistry, config TransportConfig) *Transport { +func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config TransportConfig) *Transport { lifecycleContext, cancelLifecycle := context.WithCancel(context.Background()) return &Transport{ config: config, - nodeManager: node, - peerRegistry: registry, + nodeManager: nodeManager, + peerRegistry: peerRegistry, connections: make(map[string]*PeerConnection), messageDeduplicator: NewMessageDeduplicator(5 * time.Minute), // 5 minute TTL for dedup upgrader: websocket.Upgrader{ @@ -405,7 +405,7 @@ func (t *Transport) OnMessage(handler MessageHandler) { t.messageHandler = handler } -// pc, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) +// peerConnection, err := transport.Connect(&Peer{ID: "worker-1", Address: "127.0.0.1:9091"}) func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { // Build WebSocket URL scheme := "ws" @@ -426,7 +426,7 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { return nil, core.E("Transport.Connect", "failed to connect to peer", err) } - pc := &PeerConnection{ + peerConnection := &PeerConnection{ Peer: peer, Conn: conn, LastActivity: time.Now(), @@ -435,63 +435,63 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } - // Perform handshake with challenge-response authentication - // This also derives and stores the shared secret in pc.SharedSecret - if err := t.performHandshake(pc); err != nil { + // Perform handshake with challenge-response authentication. + // This also derives and stores the shared secret in peerConnection.SharedSecret. + if err := t.performHandshake(peerConnection); err != nil { conn.Close() return nil, core.E("Transport.Connect", "handshake failed", err) } // Store connection using the real peer ID from handshake t.mutex.Lock() - t.connections[pc.Peer.ID] = pc + t.connections[peerConnection.Peer.ID] = peerConnection t.mutex.Unlock() - logging.Debug("connected to peer", logging.Fields{"peer_id": pc.Peer.ID, "secret_len": len(pc.SharedSecret)}) + logging.Debug("connected to peer", logging.Fields{"peer_id": peerConnection.Peer.ID, "secret_len": len(peerConnection.SharedSecret)}) logging.Debug("connected peer metadata", logging.Fields{ - "peer_id": pc.Peer.ID, - "user_agent": pc.UserAgent, + "peer_id": peerConnection.Peer.ID, + "user_agent": peerConnection.UserAgent, }) // Update registry - t.peerRegistry.SetConnected(pc.Peer.ID, true) + t.peerRegistry.SetConnected(peerConnection.Peer.ID, true) // Start read loop t.waitGroup.Add(1) - go t.readLoop(pc) + go t.readLoop(peerConnection) - logging.Debug("started readLoop for peer", logging.Fields{"peer_id": pc.Peer.ID}) + logging.Debug("started readLoop for peer", logging.Fields{"peer_id": peerConnection.Peer.ID}) // Start keepalive t.waitGroup.Add(1) - go t.keepalive(pc) + go t.keepalive(peerConnection) - return pc, nil + return peerConnection, nil } // err := transport.Send("worker-1", message) -func (t *Transport) Send(peerID string, msg *Message) error { +func (t *Transport) Send(peerID string, message *Message) error { t.mutex.RLock() - pc, exists := t.connections[peerID] + peerConnection, exists := t.connections[peerID] t.mutex.RUnlock() if !exists { return core.E("Transport.Send", "peer "+peerID+" not connected", nil) } - return pc.Send(msg) + return peerConnection.Send(message) } -// for pc := range transport.Connections() { -// _ = pc +// for peerConnection := range transport.Connections() { +// _ = peerConnection // } func (t *Transport) Connections() iter.Seq[*PeerConnection] { return func(yield func(*PeerConnection) bool) { t.mutex.RLock() defer t.mutex.RUnlock() - for _, pc := range t.connections { - if !yield(pc) { + for _, peerConnection := range t.connections { + if !yield(peerConnection) { return } } @@ -499,15 +499,15 @@ func (t *Transport) Connections() iter.Seq[*PeerConnection] { } // err := transport.Broadcast(announcement) -func (t *Transport) Broadcast(msg *Message) error { +func (t *Transport) Broadcast(message *Message) error { conns := slices.Collect(t.Connections()) var lastErr error - for _, pc := range conns { - if pc.Peer != nil && pc.Peer.ID == msg.From { + for _, peerConnection := range conns { + if peerConnection.Peer != nil && peerConnection.Peer.ID == message.From { continue } - if err := pc.Send(msg); err != nil { + if err := peerConnection.Send(message); err != nil { lastErr = err } } diff --git a/ueps/packet.go b/ueps/packet.go index f65a7b4..ad50f9c 100644 --- a/ueps/packet.go +++ b/ueps/packet.go @@ -51,60 +51,60 @@ func NewBuilder(intentID uint8, payload []byte) *PacketBuilder { // frame, err := builder.MarshalAndSign(sharedSecret) func (p *PacketBuilder) MarshalAndSign(sharedSecret []byte) ([]byte, error) { - buf := new(bytes.Buffer) + buffer := new(bytes.Buffer) - if err := writeTLV(buf, TagVersion, []byte{p.Header.Version}); err != nil { + if err := writeTLV(buffer, TagVersion, []byte{p.Header.Version}); err != nil { return nil, err } - if err := writeTLV(buf, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { + if err := writeTLV(buffer, TagCurrentLayer, []byte{p.Header.CurrentLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { + if err := writeTLV(buffer, TagTargetLayer, []byte{p.Header.TargetLayer}); err != nil { return nil, err } - if err := writeTLV(buf, TagIntent, []byte{p.Header.IntentID}); err != nil { + if err := writeTLV(buffer, TagIntent, []byte{p.Header.IntentID}); err != nil { return nil, err } - tsBuf := make([]byte, 2) - binary.BigEndian.PutUint16(tsBuf, p.Header.ThreatScore) - if err := writeTLV(buf, TagThreatScore, tsBuf); err != nil { + threatScoreBytes := make([]byte, 2) + binary.BigEndian.PutUint16(threatScoreBytes, p.Header.ThreatScore) + if err := writeTLV(buffer, TagThreatScore, threatScoreBytes); err != nil { return nil, err } mac := hmac.New(sha256.New, sharedSecret) - mac.Write(buf.Bytes()) + mac.Write(buffer.Bytes()) mac.Write(p.Payload) signature := mac.Sum(nil) - if err := writeTLV(buf, TagHMAC, signature); err != nil { + if err := writeTLV(buffer, TagHMAC, signature); err != nil { return nil, err } - if err := writeTLV(buf, TagPayload, p.Payload); err != nil { + if err := writeTLV(buffer, TagPayload, p.Payload); err != nil { return nil, err } - return buf.Bytes(), nil + return buffer.Bytes(), nil } -// writeTLV(&buf, TagPayload, []byte("hello")) -func writeTLV(w io.Writer, tag uint8, value []byte) error { +// writeTLV(&buffer, TagPayload, []byte("hello")) +func writeTLV(writer io.Writer, tag uint8, value []byte) error { if len(value) > 65535 { return core.E("ueps.writeTLV", "TLV value too large for 2-byte length header", nil) } - if _, err := w.Write([]byte{tag}); err != nil { + if _, err := writer.Write([]byte{tag}); err != nil { return err } lenBuf := make([]byte, 2) binary.BigEndian.PutUint16(lenBuf, uint16(len(value))) - if _, err := w.Write(lenBuf); err != nil { + if _, err := writer.Write(lenBuf); err != nil { return err } - if _, err := w.Write(value); err != nil { + if _, err := writer.Write(value); err != nil { return err } return nil From 3733e61962d1c7454b88e3aa8421ca1cdd246eab Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:04:41 +0000 Subject: [PATCH 08/12] refactor(node): tighten AX comments across public APIs Co-Authored-By: Virgil --- node/bundle.go | 36 +++-------- node/controller.go | 36 +++-------- node/dispatcher.go | 13 ++-- node/identity.go | 47 ++++----------- node/levin/header.go | 13 +--- node/levin/storage.go | 117 +++++++++++------------------------- node/levin/varint.go | 11 +--- node/message.go | 82 +++++++------------------ node/peer.go | 136 ++++++++++++++---------------------------- node/protocol.go | 24 ++------ node/transport.go | 29 +++------ 11 files changed, 156 insertions(+), 388 deletions(-) diff --git a/node/bundle.go b/node/bundle.go index de52eec..f1c7342 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -14,9 +14,7 @@ import ( "forge.lthn.ai/Snider/Borg/pkg/tim" ) -// BundleType defines the type of deployment bundle. -// -// bundleType := BundleProfile +// bundleType := BundleProfile type BundleType string const ( @@ -28,9 +26,7 @@ const ( BundleFull BundleType = "full" ) -// Bundle represents a deployment bundle for P2P transfer. -// -// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} +// bundle := &Bundle{Type: BundleProfile, Name: "xmrig", Data: []byte("{}")} type Bundle struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -38,9 +34,7 @@ type Bundle struct { Checksum string `json:"checksum"` // SHA-256 of Data } -// BundleManifest describes the contents of a bundle. -// -// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} +// manifest := BundleManifest{Name: "xmrig", Type: BundleMiner} type BundleManifest struct { Type BundleType `json:"type"` Name string `json:"name"` @@ -50,9 +44,7 @@ type BundleManifest struct { CreatedAt string `json:"createdAt"` } -// CreateProfileBundle creates an encrypted bundle containing a mining profile. -// -// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") +// bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { // Create a TIM with just the profile config timBundle, err := tim.New() @@ -78,9 +70,7 @@ func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bun }, nil } -// CreateProfileBundleUnencrypted creates a plain JSON bundle (for testing or trusted networks). -// -// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") +// bundle, err := CreateProfileBundleUnencrypted(profileJSON, "xmrig-default") func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, error) { checksum := calculateChecksum(profileJSON) @@ -92,9 +82,7 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e }, nil } -// CreateMinerBundle creates an encrypted bundle containing a miner binary and optional profile. -// -// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") +// bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { // Read miner binary minerContent, err := filesystemRead(minerPath) @@ -144,9 +132,7 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo }, nil } -// ExtractProfileBundle decrypts and extracts a profile bundle. -// -// profileJSON, err := ExtractProfileBundle(bundle, "password") +// profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -167,9 +153,7 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { return timBundle.Config, nil } -// ExtractMinerBundle decrypts and extracts a miner bundle, returning the miner path and profile. -// -// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") +// minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { @@ -197,9 +181,7 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string return minerPath, timBundle.Config, nil } -// VerifyBundle checks if a bundle's checksum is valid. -// -// ok := VerifyBundle(bundle) +// ok := VerifyBundle(bundle) func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } diff --git a/node/controller.go b/node/controller.go index 3d4ebc9..17bbc22 100644 --- a/node/controller.go +++ b/node/controller.go @@ -21,9 +21,7 @@ type Controller struct { pendingRequests map[string]chan *Message // message ID -> response channel } -// NewController wires a controller to a node manager, peer registry, and transport. -// -// controller := NewController(nodeManager, peerRegistry, transport) +// controller := NewController(nodeManager, peerRegistry, transport) func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transport *Transport) *Controller { c := &Controller{ nodeManager: nodeManager, @@ -117,9 +115,7 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D } } -// GetRemoteStats requests miner statistics from a remote peer. -// -// stats, err := controller.GetRemoteStats("worker-1") +// stats, err := controller.GetRemoteStats("worker-1") func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -144,9 +140,7 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { return &stats, nil } -// StartRemoteMiner requests a remote peer to start a miner with a given profile. -// -// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) +// err := controller.StartRemoteMiner("worker-1", "xmrig", "profile-1", nil) func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, configOverride RawMessage) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -185,9 +179,7 @@ func (c *Controller) StartRemoteMiner(peerID, minerType, profileID string, confi return nil } -// StopRemoteMiner requests a remote peer to stop a miner. -// -// err := controller.StopRemoteMiner("worker-1", "xmrig-0") +// err := controller.StopRemoteMiner("worker-1", "xmrig-0") func (c *Controller) StopRemoteMiner(peerID, minerName string) error { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -220,9 +212,7 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error { return nil } -// GetRemoteLogs requests console logs from a remote miner. -// -// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) +// logs, err := controller.GetRemoteLogs("worker-1", "xmrig-0", 100) func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -252,9 +242,7 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin return logs.Lines, nil } -// GetAllStats fetches stats from all connected peers. -// -// statsByPeerID := controller.GetAllStats() +// statsByPeerID := controller.GetAllStats() func (c *Controller) GetAllStats() map[string]*StatsPayload { results := make(map[string]*StatsPayload) var mu sync.Mutex @@ -283,9 +271,7 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload { return results } -// PingPeer sends a ping to a peer and refreshes that peer's metrics. -// -// rttMilliseconds, err := controller.PingPeer("worker-1") +// rttMilliseconds, err := controller.PingPeer("worker-1") func (c *Controller) PingPeer(peerID string) (float64, error) { identity := c.nodeManager.GetIdentity() if identity == nil { @@ -323,9 +309,7 @@ func (c *Controller) PingPeer(peerID string) (float64, error) { return rtt, nil } -// ConnectToPeer opens a transport connection to a peer. -// -// err := controller.ConnectToPeer("worker-1") +// err := controller.ConnectToPeer("worker-1") func (c *Controller) ConnectToPeer(peerID string) error { peer := c.peerRegistry.GetPeer(peerID) if peer == nil { @@ -336,9 +320,7 @@ func (c *Controller) ConnectToPeer(peerID string) error { return err } -// DisconnectFromPeer closes the active connection to a peer. -// -// err := controller.DisconnectFromPeer("worker-1") +// err := controller.DisconnectFromPeer("worker-1") func (c *Controller) DisconnectFromPeer(peerID string) error { conn := c.transport.GetConnection(peerID) if conn == nil { diff --git a/node/dispatcher.go b/node/dispatcher.go index 1307713..faedf4b 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -22,10 +22,7 @@ const ( IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) -// IntentHandler processes a UEPS packet that has been routed by intent. -// Implementations receive the fully parsed and HMAC-verified packet. -// -// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } +// var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } type IntentHandler func(packet *ueps.ParsedPacket) error // dispatcher := NewDispatcher() @@ -58,10 +55,10 @@ func (d *Dispatcher) RegisterHandler(intentID byte, handler IntentHandler) { }) } -// for intentID, handler := range dispatcher.Handlers() { -// _ = intentID -// _ = handler -// } +// for intentID, handler := range dispatcher.Handlers() { +// _ = intentID +// _ = handler +// } func (d *Dispatcher) Handlers() iter.Seq2[byte, IntentHandler] { return func(yield func(byte, IntentHandler) bool) { d.mu.RLock() diff --git a/node/identity.go b/node/identity.go index 1309281..ae5d1f1 100644 --- a/node/identity.go +++ b/node/identity.go @@ -16,12 +16,10 @@ import ( "github.com/adrg/xdg" ) -// ChallengeSize is the size of the challenge in bytes +// challenge := make([]byte, ChallengeSize) const ChallengeSize = 32 -// GenerateChallenge creates a random challenge for authentication. -// -// challenge, err := GenerateChallenge() +// challenge, err := GenerateChallenge() func GenerateChallenge() ([]byte, error) { challenge := make([]byte, ChallengeSize) if _, err := rand.Read(challenge); err != nil { @@ -30,27 +28,20 @@ func GenerateChallenge() ([]byte, error) { return challenge, nil } -// SignChallenge creates an HMAC signature of a challenge using a shared secret. -// The signature proves possession of the shared secret without revealing it. -// -// signature := SignChallenge(challenge, sharedSecret) +// signature := SignChallenge(challenge, sharedSecret) func SignChallenge(challenge []byte, sharedSecret []byte) []byte { mac := hmac.New(sha256.New, sharedSecret) mac.Write(challenge) return mac.Sum(nil) } -// VerifyChallenge verifies that a challenge response was signed with the correct shared secret. -// -// ok := VerifyChallenge(challenge, signature, sharedSecret) +// ok := VerifyChallenge(challenge, signature, sharedSecret) func VerifyChallenge(challenge, response, sharedSecret []byte) bool { expected := SignChallenge(challenge, sharedSecret) return hmac.Equal(response, expected) } -// NodeRole defines the operational mode of a node. -// -// role := RoleWorker +// role := RoleWorker type NodeRole string const ( @@ -62,9 +53,7 @@ const ( RoleDual NodeRole = "dual" ) -// NodeIdentity represents the public identity of a node. -// -// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} +// identity := NodeIdentity{Name: "worker-1", Role: RoleWorker} type NodeIdentity struct { ID string `json:"id"` // Derived from public key (first 16 bytes hex) Name string `json:"name"` // Human-friendly name @@ -83,9 +72,7 @@ type NodeManager struct { mu sync.RWMutex } -// NewNodeManager loads the default node identity store. -// -// nodeManager, err := NewNodeManager() +// nodeManager, err := NewNodeManager() func NewNodeManager() (*NodeManager, error) { keyPath, err := xdg.DataFile("lethean-desktop/node/private.key") if err != nil { @@ -100,12 +87,10 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// NewNodeManagerFromPaths loads or creates a node identity store at explicit paths. -// // Missing files are treated as a fresh install; malformed or partial identity // state is returned as an error so callers can handle it explicitly. // -// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, @@ -131,9 +116,7 @@ func (n *NodeManager) HasIdentity() bool { return n.identity != nil } -// GetIdentity returns a copy of the loaded node identity. -// -// identity := nodeManager.GetIdentity() +// identity := nodeManager.GetIdentity() func (n *NodeManager) GetIdentity() *NodeIdentity { n.mu.RLock() defer n.mu.RUnlock() @@ -145,9 +128,7 @@ func (n *NodeManager) GetIdentity() *NodeIdentity { return &identity } -// GenerateIdentity writes a new node identity for the given name and role. -// -// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) +// err := nodeManager.GenerateIdentity("worker-1", RoleWorker) func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { n.mu.Lock() defer n.mu.Unlock() @@ -187,9 +168,7 @@ func (n *NodeManager) GenerateIdentity(name string, role NodeRole) error { return nil } -// DeriveSharedSecret hashes an X25519 shared secret for use as a symmetric key. -// -// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) +// sharedSecret, err := nodeManager.DeriveSharedSecret(peer.PublicKey) func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error) { n.mu.RLock() defer n.mu.RUnlock() @@ -292,9 +271,7 @@ func (n *NodeManager) loadIdentity() error { return nil } -// Delete removes the node identity and key files from disk. -// -// err := nodeManager.Delete() +// err := nodeManager.Delete() func (n *NodeManager) Delete() error { n.mu.Lock() defer n.mu.Unlock() diff --git a/node/levin/header.go b/node/levin/header.go index ee4d9ab..911435a 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -47,9 +47,7 @@ var ( ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) -// Header is the 33-byte packed header that prefixes every Levin message. -// -// header := Header{Command: CommandHandshake, ExpectResponse: true} +// header := Header{Command: CommandHandshake, ExpectResponse: true} type Header struct { Signature uint64 PayloadSize uint64 @@ -60,9 +58,7 @@ type Header struct { ProtocolVersion uint32 } -// EncodeHeader serialises header into a fixed-size 33-byte array (little-endian). -// -// encoded := EncodeHeader(header) +// encoded := EncodeHeader(header) func EncodeHeader(header *Header) [HeaderSize]byte { var headerBytes [HeaderSize]byte binary.LittleEndian.PutUint64(headerBytes[0:8], header.Signature) @@ -79,10 +75,7 @@ func EncodeHeader(header *Header) [HeaderSize]byte { return headerBytes } -// DecodeHeader deserialises a 33-byte array into a Header, validating -// the magic signature. -// -// header, err := DecodeHeader(headerBytes) +// header, err := DecodeHeader(headerBytes) func DecodeHeader(headerBytes [HeaderSize]byte) (Header, error) { var header Header header.Signature = binary.LittleEndian.Uint64(headerBytes[0:8]) diff --git a/node/levin/storage.go b/node/levin/storage.go index 9a6d4ed..08aa7ca 100644 --- a/node/levin/storage.go +++ b/node/levin/storage.go @@ -48,16 +48,10 @@ var ( ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil) ) -// Section is an ordered map of named values forming a portable storage section. -// Field iteration order is always alphabetical by key for deterministic encoding. -// -// section := Section{"id": StringValue([]byte("peer-1"))} +// section := Section{"id": StringValue([]byte("peer-1"))} type Section map[string]Value -// Value holds a typed portable storage value. Use the constructor functions -// (Uint64Value, StringValue, ObjectValue, etc.) to create instances. -// -// value := StringValue([]byte("peer-1")) +// value := StringValue([]byte("peer-1")) type Value struct { Type uint8 @@ -80,94 +74,62 @@ type Value struct { // Scalar constructors // --------------------------------------------------------------------------- -// Uint64Value creates a Value of TypeUint64. -// -// value := Uint64Value(42) +// value := Uint64Value(42) func Uint64Value(value uint64) Value { return Value{Type: TypeUint64, uintVal: value} } -// Uint32Value creates a Value of TypeUint32. -// -// value := Uint32Value(42) +// value := Uint32Value(42) func Uint32Value(value uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(value)} } -// Uint16Value creates a Value of TypeUint16. -// -// value := Uint16Value(42) +// value := Uint16Value(42) func Uint16Value(value uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(value)} } -// Uint8Value creates a Value of TypeUint8. -// -// value := Uint8Value(42) +// value := Uint8Value(42) func Uint8Value(value uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(value)} } -// Int64Value creates a Value of TypeInt64. -// -// value := Int64Value(42) +// value := Int64Value(42) func Int64Value(value int64) Value { return Value{Type: TypeInt64, intVal: value} } -// Int32Value creates a Value of TypeInt32. -// -// value := Int32Value(42) +// value := Int32Value(42) func Int32Value(value int32) Value { return Value{Type: TypeInt32, intVal: int64(value)} } -// Int16Value creates a Value of TypeInt16. -// -// value := Int16Value(42) +// value := Int16Value(42) func Int16Value(value int16) Value { return Value{Type: TypeInt16, intVal: int64(value)} } -// Int8Value creates a Value of TypeInt8. -// -// value := Int8Value(42) +// value := Int8Value(42) func Int8Value(value int8) Value { return Value{Type: TypeInt8, intVal: int64(value)} } -// BoolValue creates a Value of TypeBool. -// -// value := BoolValue(true) +// value := BoolValue(true) func BoolValue(value bool) Value { return Value{Type: TypeBool, boolVal: value} } -// DoubleValue creates a Value of TypeDouble. -// -// value := DoubleValue(3.14) +// value := DoubleValue(3.14) func DoubleValue(value float64) Value { return Value{Type: TypeDouble, floatVal: value} } -// StringValue creates a Value of TypeString. The slice is not copied. -// -// value := StringValue([]byte("hello")) +// value := StringValue([]byte("hello")) func StringValue(value []byte) Value { return Value{Type: TypeString, bytesVal: value} } -// ObjectValue creates a Value of TypeObject wrapping a nested Section. -// -// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) +// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}) func ObjectValue(section Section) Value { return Value{Type: TypeObject, objectVal: section} } // --------------------------------------------------------------------------- // Array constructors // --------------------------------------------------------------------------- -// Uint64ArrayValue creates a typed array of uint64 values. -// -// value := Uint64ArrayValue([]uint64{1, 2, 3}) +// value := Uint64ArrayValue([]uint64{1, 2, 3}) func Uint64ArrayValue(values []uint64) Value { return Value{Type: ArrayFlag | TypeUint64, uint64Array: values} } -// Uint32ArrayValue creates a typed array of uint32 values. -// -// value := Uint32ArrayValue([]uint32{1, 2, 3}) +// value := Uint32ArrayValue([]uint32{1, 2, 3}) func Uint32ArrayValue(values []uint32) Value { return Value{Type: ArrayFlag | TypeUint32, uint32Array: values} } -// StringArrayValue creates a typed array of byte-string values. -// -// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) +// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")}) func StringArrayValue(values [][]byte) Value { return Value{Type: ArrayFlag | TypeString, stringArray: values} } -// ObjectArrayValue creates a typed array of Section values. -// -// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) +// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}) func ObjectArrayValue(values []Section) Value { return Value{Type: ArrayFlag | TypeObject, objectArray: values} } @@ -176,7 +138,7 @@ func ObjectArrayValue(values []Section) Value { // Scalar accessors // --------------------------------------------------------------------------- -// AsUint64 returns the uint64 value or an error on type mismatch. +// value, err := Uint64Value(42).AsUint64() func (v Value) AsUint64() (uint64, error) { if v.Type != TypeUint64 { return 0, ErrorStorageTypeMismatch @@ -184,7 +146,7 @@ func (v Value) AsUint64() (uint64, error) { return v.uintVal, nil } -// AsUint32 returns the uint32 value or an error on type mismatch. +// value, err := Uint32Value(42).AsUint32() func (v Value) AsUint32() (uint32, error) { if v.Type != TypeUint32 { return 0, ErrorStorageTypeMismatch @@ -192,7 +154,7 @@ func (v Value) AsUint32() (uint32, error) { return uint32(v.uintVal), nil } -// AsUint16 returns the uint16 value or an error on type mismatch. +// value, err := Uint16Value(42).AsUint16() func (v Value) AsUint16() (uint16, error) { if v.Type != TypeUint16 { return 0, ErrorStorageTypeMismatch @@ -200,7 +162,7 @@ func (v Value) AsUint16() (uint16, error) { return uint16(v.uintVal), nil } -// AsUint8 returns the uint8 value or an error on type mismatch. +// value, err := Uint8Value(42).AsUint8() func (v Value) AsUint8() (uint8, error) { if v.Type != TypeUint8 { return 0, ErrorStorageTypeMismatch @@ -208,7 +170,7 @@ func (v Value) AsUint8() (uint8, error) { return uint8(v.uintVal), nil } -// AsInt64 returns the int64 value or an error on type mismatch. +// value, err := Int64Value(42).AsInt64() func (v Value) AsInt64() (int64, error) { if v.Type != TypeInt64 { return 0, ErrorStorageTypeMismatch @@ -216,7 +178,7 @@ func (v Value) AsInt64() (int64, error) { return v.intVal, nil } -// AsInt32 returns the int32 value or an error on type mismatch. +// value, err := Int32Value(42).AsInt32() func (v Value) AsInt32() (int32, error) { if v.Type != TypeInt32 { return 0, ErrorStorageTypeMismatch @@ -224,7 +186,7 @@ func (v Value) AsInt32() (int32, error) { return int32(v.intVal), nil } -// AsInt16 returns the int16 value or an error on type mismatch. +// value, err := Int16Value(42).AsInt16() func (v Value) AsInt16() (int16, error) { if v.Type != TypeInt16 { return 0, ErrorStorageTypeMismatch @@ -232,7 +194,7 @@ func (v Value) AsInt16() (int16, error) { return int16(v.intVal), nil } -// AsInt8 returns the int8 value or an error on type mismatch. +// value, err := Int8Value(42).AsInt8() func (v Value) AsInt8() (int8, error) { if v.Type != TypeInt8 { return 0, ErrorStorageTypeMismatch @@ -240,7 +202,7 @@ func (v Value) AsInt8() (int8, error) { return int8(v.intVal), nil } -// AsBool returns the bool value or an error on type mismatch. +// value, err := BoolValue(true).AsBool() func (v Value) AsBool() (bool, error) { if v.Type != TypeBool { return false, ErrorStorageTypeMismatch @@ -248,7 +210,7 @@ func (v Value) AsBool() (bool, error) { return v.boolVal, nil } -// AsDouble returns the float64 value or an error on type mismatch. +// value, err := DoubleValue(3.14).AsDouble() func (v Value) AsDouble() (float64, error) { if v.Type != TypeDouble { return 0, ErrorStorageTypeMismatch @@ -256,7 +218,7 @@ func (v Value) AsDouble() (float64, error) { return v.floatVal, nil } -// AsString returns the byte-string value or an error on type mismatch. +// value, err := StringValue([]byte("hello")).AsString() func (v Value) AsString() ([]byte, error) { if v.Type != TypeString { return nil, ErrorStorageTypeMismatch @@ -264,7 +226,7 @@ func (v Value) AsString() ([]byte, error) { return v.bytesVal, nil } -// AsSection returns the nested Section or an error on type mismatch. +// section, err := ObjectValue(Section{"id": StringValue([]byte("peer-1"))}).AsSection() func (v Value) AsSection() (Section, error) { if v.Type != TypeObject { return nil, ErrorStorageTypeMismatch @@ -276,7 +238,7 @@ func (v Value) AsSection() (Section, error) { // Array accessors // --------------------------------------------------------------------------- -// AsUint64Array returns the []uint64 array or an error on type mismatch. +// values, err := Uint64ArrayValue([]uint64{1, 2, 3}).AsUint64Array() func (v Value) AsUint64Array() ([]uint64, error) { if v.Type != (ArrayFlag | TypeUint64) { return nil, ErrorStorageTypeMismatch @@ -284,7 +246,7 @@ func (v Value) AsUint64Array() ([]uint64, error) { return v.uint64Array, nil } -// AsUint32Array returns the []uint32 array or an error on type mismatch. +// values, err := Uint32ArrayValue([]uint32{1, 2, 3}).AsUint32Array() func (v Value) AsUint32Array() ([]uint32, error) { if v.Type != (ArrayFlag | TypeUint32) { return nil, ErrorStorageTypeMismatch @@ -292,7 +254,7 @@ func (v Value) AsUint32Array() ([]uint32, error) { return v.uint32Array, nil } -// AsStringArray returns the [][]byte array or an error on type mismatch. +// values, err := StringArrayValue([][]byte{[]byte("a"), []byte("b")}).AsStringArray() func (v Value) AsStringArray() ([][]byte, error) { if v.Type != (ArrayFlag | TypeString) { return nil, ErrorStorageTypeMismatch @@ -300,7 +262,7 @@ func (v Value) AsStringArray() ([][]byte, error) { return v.stringArray, nil } -// AsSectionArray returns the []Section array or an error on type mismatch. +// values, err := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}}).AsSectionArray() func (v Value) AsSectionArray() ([]Section, error) { if v.Type != (ArrayFlag | TypeObject) { return nil, ErrorStorageTypeMismatch @@ -312,11 +274,7 @@ func (v Value) AsSectionArray() ([]Section, error) { // Encoder // --------------------------------------------------------------------------- -// EncodeStorage serialises a Section to the portable storage binary format, -// including the 9-byte header. Keys are sorted alphabetically to ensure -// deterministic output. -// -// data, err := EncodeStorage(section) +// data, err := EncodeStorage(section) func EncodeStorage(section Section) ([]byte, error) { buffer := make([]byte, 0, 256) @@ -486,10 +444,7 @@ func encodeArray(buf []byte, v Value) ([]byte, error) { // Decoder // --------------------------------------------------------------------------- -// DecodeStorage deserialises portable storage binary data (including the -// 9-byte header) into a Section. -// -// section, err := DecodeStorage(data) +// section, err := DecodeStorage(data) func DecodeStorage(data []byte) (Section, error) { if len(data) < StorageHeaderSize { return nil, ErrorStorageTruncated diff --git a/node/levin/varint.go b/node/levin/varint.go index 2d26512..b245833 100644 --- a/node/levin/varint.go +++ b/node/levin/varint.go @@ -28,11 +28,7 @@ var ErrorVarintTruncated = core.E("levin", "truncated varint", nil) // ErrorVarintOverflow is returned when the value is too large to encode. var ErrorVarintOverflow = core.E("levin", "varint overflow", nil) -// PackVarint encodes value using the epee portable-storage varint scheme. -// The low two bits of the first byte indicate the total encoded width; -// the remaining bits carry the value in little-endian order. -// -// encoded := PackVarint(42) +// encoded := PackVarint(42) func PackVarint(value uint64) []byte { switch { case value <= varintMax1: @@ -55,10 +51,7 @@ func PackVarint(value uint64) []byte { } } -// UnpackVarint decodes one epee portable-storage varint from buffer. -// It returns the decoded value, the number of bytes consumed, and any error. -// -// value, err := UnpackVarint(buffer) +// value, err := UnpackVarint(buffer) func UnpackVarint(buffer []byte) (value uint64, bytesConsumed int, err error) { if len(buffer) == 0 { return 0, 0, ErrorVarintTruncated diff --git a/node/message.go b/node/message.go index 1640fde..eb21c41 100644 --- a/node/message.go +++ b/node/message.go @@ -22,14 +22,10 @@ const ( // versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} -// RawMessage stores an already-encoded JSON payload for deferred decoding. -// -// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) +// payload := RawMessage(`{"pool":"pool.example.com:3333"}`) type RawMessage []byte -// MarshalJSON preserves the raw JSON payload when the message is encoded. -// -// data, err := RawMessage(`{"ok":true}`).MarshalJSON() +// data, err := RawMessage(`{"ok":true}`).MarshalJSON() func (m RawMessage) MarshalJSON() ([]byte, error) { if m == nil { return []byte("null"), nil @@ -38,10 +34,8 @@ func (m RawMessage) MarshalJSON() ([]byte, error) { return m, nil } -// UnmarshalJSON stores the raw JSON payload bytes without decoding them. -// -// var payload RawMessage -// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) +// var payload RawMessage +// _ = payload.UnmarshalJSON([]byte(`{"ok":true}`)) func (m *RawMessage) UnmarshalJSON(data []byte) error { if m == nil { return core.E("node.RawMessage.UnmarshalJSON", "raw message target is nil", nil) @@ -51,9 +45,7 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error { return nil } -// IsProtocolVersionSupported checks if a given version is supported. -// -// ok := IsProtocolVersionSupported("1.0") +// ok := IsProtocolVersionSupported("1.0") func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } @@ -90,9 +82,7 @@ const ( MessageError MessageType = "error" ) -// Message represents a P2P message between nodes. -// -// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) +// message, err := NewMessage(MessagePing, "controller", "worker", PingPayload{SentAt: time.Now().UnixMilli()}) type Message struct { ID string `json:"id"` // UUID Type MessageType `json:"type"` @@ -103,9 +93,7 @@ type Message struct { ReplyTo string `json:"replyTo,omitempty"` // ID of message being replied to } -// NewMessage builds a message with a generated ID and timestamp. -// -// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) +// message, err := NewMessage(MessagePing, "controller", "worker-1", PingPayload{SentAt: 42}) func NewMessage(messageType MessageType, from, to string, payload any) (*Message, error) { var payloadBytes RawMessage if payload != nil { @@ -151,18 +139,14 @@ func (m *Message) ParsePayload(target any) error { // --- Payload Types --- -// HandshakePayload is sent during connection establishment. -// -// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} +// payload := HandshakePayload{Identity: NodeIdentity{Name: "worker-1"}, Version: ProtocolVersion} type HandshakePayload struct { Identity NodeIdentity `json:"identity"` Challenge []byte `json:"challenge,omitempty"` // Random bytes for auth Version string `json:"version"` // Protocol version } -// HandshakeAckPayload is the response to a handshake. -// -// ack := HandshakeAckPayload{Accepted: true} +// ack := HandshakeAckPayload{Accepted: true} type HandshakeAckPayload struct { Identity NodeIdentity `json:"identity"` ChallengeResponse []byte `json:"challengeResponse,omitempty"` @@ -170,49 +154,37 @@ type HandshakeAckPayload struct { Reason string `json:"reason,omitempty"` // If not accepted } -// PingPayload for keepalive/latency measurement. -// -// payload := PingPayload{SentAt: 42} +// payload := PingPayload{SentAt: 42} type PingPayload struct { SentAt int64 `json:"sentAt"` // Unix timestamp in milliseconds } -// PongPayload response to ping. -// -// payload := PongPayload{SentAt: 42, ReceivedAt: 43} +// payload := PongPayload{SentAt: 42, ReceivedAt: 43} type PongPayload struct { SentAt int64 `json:"sentAt"` // Echo of ping's sentAt ReceivedAt int64 `json:"receivedAt"` // When ping was received } -// StartMinerPayload requests starting a miner. -// -// payload := StartMinerPayload{MinerType: "xmrig"} +// payload := StartMinerPayload{MinerType: "xmrig"} type StartMinerPayload struct { MinerType string `json:"minerType"` // Required: miner type (e.g., "xmrig", "tt-miner") ProfileID string `json:"profileId,omitempty"` Config RawMessage `json:"config,omitempty"` // Override profile config } -// StopMinerPayload requests stopping a miner. -// -// payload := StopMinerPayload{MinerName: "xmrig-0"} +// payload := StopMinerPayload{MinerName: "xmrig-0"} type StopMinerPayload struct { MinerName string `json:"minerName"` } -// MinerAckPayload acknowledges a miner start/stop operation. -// -// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} +// ack := MinerAckPayload{Success: true, MinerName: "xmrig-0"} type MinerAckPayload struct { Success bool `json:"success"` MinerName string `json:"minerName,omitempty"` Error string `json:"error,omitempty"` } -// MinerStatsItem represents stats for a single miner. -// -// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} +// miner := MinerStatsItem{Name: "xmrig-0", Hashrate: 1200} type MinerStatsItem struct { Name string `json:"name"` Type string `json:"type"` @@ -225,9 +197,7 @@ type MinerStatsItem struct { CPUThreads int `json:"cpuThreads,omitempty"` } -// StatsPayload contains miner statistics. -// -// stats := StatsPayload{NodeID: "worker-1"} +// stats := StatsPayload{NodeID: "worker-1"} type StatsPayload struct { NodeID string `json:"nodeId"` NodeName string `json:"nodeName"` @@ -235,27 +205,21 @@ type StatsPayload struct { Uptime int64 `json:"uptime"` // Node uptime in seconds } -// LogsRequestPayload requests console logs from a miner. -// -// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} +// payload := LogsRequestPayload{MinerName: "xmrig-0", Lines: 100} type LogsRequestPayload struct { MinerName string `json:"minerName"` Lines int `json:"lines"` // Number of lines to fetch Since int64 `json:"since,omitempty"` // Unix timestamp, logs after this time } -// LogsPayload contains console log lines. -// -// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} +// payload := LogsPayload{MinerName: "xmrig-0", Lines: []string{"started"}} type LogsPayload struct { MinerName string `json:"minerName"` Lines []string `json:"lines"` HasMore bool `json:"hasMore"` // More logs available } -// DeployPayload contains a deployment bundle. -// -// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} +// payload := DeployPayload{Name: "xmrig", BundleType: string(BundleMiner)} type DeployPayload struct { BundleType string `json:"type"` // "profile" | "miner" | "full" Data []byte `json:"data"` // STIM-encrypted bundle @@ -263,18 +227,14 @@ type DeployPayload struct { Name string `json:"name"` // Profile or miner name } -// DeployAckPayload acknowledges a deployment. -// -// ack := DeployAckPayload{Success: true, Name: "xmrig"} +// ack := DeployAckPayload{Success: true, Name: "xmrig"} type DeployAckPayload struct { Success bool `json:"success"` Name string `json:"name,omitempty"` Error string `json:"error,omitempty"` } -// ErrorPayload contains error information. -// -// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} +// payload := ErrorPayload{Code: ErrorCodeOperationFailed, Message: "start failed"} type ErrorPayload struct { Code int `json:"code"` Message string `json:"message"` diff --git a/node/peer.go b/node/peer.go index b44348a..42f8801 100644 --- a/node/peer.go +++ b/node/peer.go @@ -15,16 +15,14 @@ import ( "github.com/adrg/xdg" ) -// Peer represents a known remote node. -// // peer := &Peer{ -// ID: "worker-1", -// Name: "Worker 1", -// Address: "127.0.0.1:9101", -// PingMilliseconds: 42.5, -// GeographicKilometres: 100, -// Score: 80, -// } +// ID: "worker-1", +// Name: "Worker 1", +// Address: "127.0.0.1:9101", +// PingMilliseconds: 42.5, +// GeographicKilometres: 100, +// Score: 80, +// } type Peer struct { ID string `json:"id"` Name string `json:"name"` @@ -47,9 +45,7 @@ type Peer struct { // peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second -// PeerAuthMode controls how unknown peers are handled -// -// mode := PeerAuthAllowlist +// mode := PeerAuthAllowlist type PeerAuthMode int const ( @@ -125,9 +121,7 @@ var ( scoreWeight = 1.2 ) -// NewPeerRegistry loads the default peer registry. -// -// peerRegistry, err := NewPeerRegistry() +// peerRegistry, err := NewPeerRegistry() func NewPeerRegistry() (*PeerRegistry, error) { peersPath, err := xdg.ConfigFile("lethean-desktop/peers.json") if err != nil { @@ -137,12 +131,10 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// NewPeerRegistryFromPath loads or creates a peer registry at an explicit path. -// // Missing files are treated as an empty registry; malformed registry files are // returned as errors so callers can repair the persisted state. // -// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -165,9 +157,7 @@ func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { return pr, nil } -// SetAuthMode changes how unknown peers are handled. -// -// registry.SetAuthMode(PeerAuthAllowlist) +// registry.SetAuthMode(PeerAuthAllowlist) func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -175,18 +165,14 @@ func (r *PeerRegistry) SetAuthMode(mode PeerAuthMode) { logging.Info("peer auth mode changed", logging.Fields{"mode": mode}) } -// GetAuthMode returns the current authentication mode. -// -// mode := registry.GetAuthMode() +// mode := registry.GetAuthMode() func (r *PeerRegistry) GetAuthMode() PeerAuthMode { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.authMode } -// AllowPublicKey adds a public key to the allowlist. -// -// registry.AllowPublicKey(peer.PublicKey) +// registry.AllowPublicKey(peer.PublicKey) func (r *PeerRegistry) AllowPublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -194,9 +180,7 @@ func (r *PeerRegistry) AllowPublicKey(publicKey string) { logging.Debug("public key added to allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// RevokePublicKey removes a public key from the allowlist. -// -// registry.RevokePublicKey(peer.PublicKey) +// registry.RevokePublicKey(peer.PublicKey) func (r *PeerRegistry) RevokePublicKey(publicKey string) { r.allowedPublicKeyMu.Lock() defer r.allowedPublicKeyMu.Unlock() @@ -204,20 +188,17 @@ func (r *PeerRegistry) RevokePublicKey(publicKey string) { logging.Debug("public key removed from allowlist", logging.Fields{"key": safeKeyPrefix(publicKey)}) } -// IsPublicKeyAllowed checks if a public key is in the allowlist. -// -// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) +// allowed := registry.IsPublicKeyAllowed(peer.PublicKey) func (r *PeerRegistry) IsPublicKeyAllowed(publicKey string) bool { r.allowedPublicKeyMu.RLock() defer r.allowedPublicKeyMu.RUnlock() return r.allowedPublicKeys[publicKey] } -// IsPeerAllowed checks if a peer is allowed to connect based on auth mode. // Returns true when AuthMode is Open (all allowed), or when Allowlist mode is active // and the peer is pre-registered or its public key is in the allowlist. // -// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) +// allowed := registry.IsPeerAllowed(peer.ID, peer.PublicKey) func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { r.allowedPublicKeyMu.RLock() authMode := r.authMode @@ -242,18 +223,14 @@ func (r *PeerRegistry) IsPeerAllowed(peerID string, publicKey string) bool { return keyAllowed } -// ListAllowedPublicKeys returns all allowlisted public keys. -// -// keys := registry.ListAllowedPublicKeys() +// keys := registry.ListAllowedPublicKeys() func (r *PeerRegistry) ListAllowedPublicKeys() []string { return slices.Collect(r.AllowedPublicKeys()) } -// AllowedPublicKeys returns an iterator over all allowlisted public keys. -// // for key := range registry.AllowedPublicKeys() { -// log.Printf("allowed: %s", key[:16]) -// } +// log.Printf("allowed: %s", key[:16]) +// } func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { return func(yield func(string) bool) { r.allowedPublicKeyMu.RLock() @@ -267,10 +244,9 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// AddPeer adds a new peer to the registry. // Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. // -// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) +// err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { r.mu.Lock() @@ -306,10 +282,9 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { return nil } -// UpdatePeer updates an existing peer's information. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) +// err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { r.mu.Lock() @@ -326,10 +301,9 @@ func (r *PeerRegistry) UpdatePeer(peer *Peer) error { return nil } -// RemovePeer removes a peer from the registry. // Persistence is debounced. Call Close() to flush before shutdown. // -// err := registry.RemovePeer("worker-1") +// err := registry.RemovePeer("worker-1") func (r *PeerRegistry) RemovePeer(id string) error { r.mu.Lock() @@ -346,9 +320,7 @@ func (r *PeerRegistry) RemovePeer(id string) error { return nil } -// GetPeer returns a copy of the peer with the supplied ID. -// -// peer := registry.GetPeer("worker-1") +// peer := registry.GetPeer("worker-1") func (r *PeerRegistry) GetPeer(id string) *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -363,13 +335,16 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return &peerCopy } -// ListPeers returns all registered peers. +// peers := registry.ListPeers() func (r *PeerRegistry) ListPeers() []*Peer { return slices.Collect(r.Peers()) } -// Peers returns an iterator over all registered peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.Peers() { +// _ = peer +// } func (r *PeerRegistry) Peers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -384,10 +359,7 @@ func (r *PeerRegistry) Peers() iter.Seq[*Peer] { } } -// UpdateMetrics updates a peer's performance metrics. -// -// registry.UpdateMetrics("worker-1", 42.5, 100, 3) -// +// registry.UpdateMetrics("worker-1", 42.5, 100, 3) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilometres float64, hopCount int) error { r.mu.Lock() @@ -410,7 +382,7 @@ func (r *PeerRegistry) UpdateMetrics(id string, pingMilliseconds, geographicKilo return nil } -// UpdateScore updates a peer's reliability score. +// registry.UpdateScore("worker-1", 90) // Note: Persistence is debounced. Call Close() to flush before shutdown. func (r *PeerRegistry) UpdateScore(id string, score float64) error { r.mu.Lock() @@ -432,9 +404,7 @@ func (r *PeerRegistry) UpdateScore(id string, score float64) error { return nil } -// SetConnected updates a peer's connection state. -// -// registry.SetConnected("worker-1", true) +// registry.SetConnected("worker-1", true) func (r *PeerRegistry) SetConnected(id string, connected bool) { r.mu.Lock() defer r.mu.Unlock() @@ -457,9 +427,7 @@ const ( ScoreDefault = 50.0 // Default score for new peers ) -// RecordSuccess records a successful interaction with a peer, improving their score. -// -// registry.RecordSuccess("worker-1") +// registry.RecordSuccess("worker-1") func (r *PeerRegistry) RecordSuccess(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -474,9 +442,7 @@ func (r *PeerRegistry) RecordSuccess(id string) { r.scheduleSave() } -// RecordFailure records a failed interaction with a peer, reducing their score. -// -// registry.RecordFailure("worker-1") +// registry.RecordFailure("worker-1") func (r *PeerRegistry) RecordFailure(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -497,9 +463,7 @@ func (r *PeerRegistry) RecordFailure(id string) { }) } -// RecordTimeout records a timeout when communicating with a peer. -// -// registry.RecordTimeout("worker-1") +// registry.RecordTimeout("worker-1") func (r *PeerRegistry) RecordTimeout(id string) { r.mu.Lock() peer, exists := r.peers[id] @@ -520,9 +484,7 @@ func (r *PeerRegistry) RecordTimeout(id string) { }) } -// GetPeersByScore returns peers sorted by score, highest first. -// -// peers := registry.GetPeersByScore() +// peers := registry.GetPeersByScore() func (r *PeerRegistry) GetPeersByScore() []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -543,11 +505,9 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return peers } -// PeersByScore returns an iterator over peers sorted by score (highest first). -// // for peer := range registry.PeersByScore() { -// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) -// } +// log.Printf("peer %s score=%.0f", peer.ID, peer.Score) +// } func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { peers := r.GetPeersByScore() @@ -559,10 +519,9 @@ func (r *PeerRegistry) PeersByScore() iter.Seq[*Peer] { } } -// SelectOptimalPeer returns the best peer based on multi-factor optimisation. // Uses Poindexter KD-tree to find the peer closest to ideal metrics (low ping, low hops, high score). // -// peer := registry.SelectOptimalPeer() +// peer := registry.SelectOptimalPeer() func (r *PeerRegistry) SelectOptimalPeer() *Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -589,9 +548,7 @@ func (r *PeerRegistry) SelectOptimalPeer() *Peer { return &peerCopy } -// SelectNearestPeers returns the n best peers based on multi-factor optimisation. -// -// peers := registry.SelectNearestPeers(3) +// peers := registry.SelectNearestPeers(3) func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { r.mu.RLock() defer r.mu.RUnlock() @@ -616,15 +573,16 @@ func (r *PeerRegistry) SelectNearestPeers(n int) []*Peer { return peers } -// GetConnectedPeers returns all currently connected peers as a slice. -// -// connectedPeers := registry.GetConnectedPeers() +// connectedPeers := registry.GetConnectedPeers() func (r *PeerRegistry) GetConnectedPeers() []*Peer { return slices.Collect(r.ConnectedPeers()) } -// ConnectedPeers returns an iterator over all currently connected peers. // Each peer is a copy to prevent mutation. +// +// for peer := range registry.ConnectedPeers() { +// _ = peer +// } func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { return func(yield func(*Peer) bool) { r.mu.RLock() @@ -641,9 +599,7 @@ func (r *PeerRegistry) ConnectedPeers() iter.Seq[*Peer] { } } -// Count returns the number of registered peers. -// -// n := registry.Count() +// n := registry.Count() func (r *PeerRegistry) Count() int { r.mu.RLock() defer r.mu.RUnlock() @@ -751,7 +707,7 @@ func (r *PeerRegistry) saveNow() error { return nil } -// Close flushes any pending changes and releases resources. +// registry.Close() func (r *PeerRegistry) Close() error { // Cancel any pending timer and save immediately if changes are queued. r.saveMutex.Lock() diff --git a/node/protocol.go b/node/protocol.go index 72c1624..23d5152 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -4,9 +4,7 @@ import ( core "dappco.re/go/core" ) -// ProtocolError represents an error from the remote peer. -// -// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} +// err := &ProtocolError{Code: ErrorCodeOperationFailed, Message: "start failed"} type ProtocolError struct { Code int Message string @@ -16,14 +14,10 @@ func (e *ProtocolError) Error() string { return core.Sprintf("remote error (%d): %s", e.Code, e.Message) } -// ResponseHandler provides helpers for handling protocol responses. -// -// handler := &ResponseHandler{} +// handler := &ResponseHandler{} type ResponseHandler struct{} -// ValidateResponse checks a response against the expected type. -// -// err := handler.ValidateResponse(resp, MessageStats) +// err := handler.ValidateResponse(resp, MessageStats) func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageType) error { if resp == nil { return core.E("ResponseHandler.ValidateResponse", "nil response", nil) @@ -46,9 +40,7 @@ func (h *ResponseHandler) ValidateResponse(resp *Message, expectedType MessageTy return nil } -// ParseResponse validates the response and parses the payload into the target. -// -// err := handler.ParseResponse(resp, MessageStats, &stats) +// err := handler.ParseResponse(resp, MessageStats, &stats) func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, target any) error { if err := h.ValidateResponse(resp, expectedType); err != nil { return err @@ -80,17 +72,13 @@ func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } -// IsProtocolError returns true if the error is a ProtocolError. -// -// ok := IsProtocolError(err) +// ok := IsProtocolError(err) func IsProtocolError(err error) bool { _, ok := err.(*ProtocolError) return ok } -// GetProtocolErrorCode returns the error code if err is a ProtocolError, otherwise returns 0. -// -// code := GetProtocolErrorCode(err) +// code := GetProtocolErrorCode(err) func GetProtocolErrorCode(err error) int { if pe, ok := err.(*ProtocolError); ok { return pe.Code diff --git a/node/transport.go b/node/transport.go index f580073..e2c74e9 100644 --- a/node/transport.go +++ b/node/transport.go @@ -39,9 +39,7 @@ const ( defaultTransportMaximumConnections = 100 ) -// TransportConfig configures the WebSocket transport. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() type TransportConfig struct { ListenAddr string // ":9091" default WebSocketPath string // "/ws" - WebSocket endpoint path @@ -53,9 +51,7 @@ type TransportConfig struct { PongTimeout time.Duration // Timeout waiting for pong } -// DefaultTransportConfig returns sensible defaults. -// -// transportConfig := DefaultTransportConfig() +// transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ ListenAddr: defaultTransportListenAddress, @@ -92,23 +88,17 @@ func (c TransportConfig) maximumConnections() int { return defaultTransportMaximumConnections } -// MessageHandler processes incoming messages. -// -// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} +// var handler MessageHandler = func(peerConnection *PeerConnection, message *Message) {} type MessageHandler func(peerConnection *PeerConnection, message *Message) -// MessageDeduplicator tracks recent message IDs to prevent duplicate processing. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) type MessageDeduplicator struct { recentMessageTimes map[string]time.Time mutex sync.RWMutex timeToLive time.Duration } -// NewMessageDeduplicator creates a deduplicator with the supplied retention window. -// -// deduplicator := NewMessageDeduplicator(5 * time.Minute) +// deduplicator := NewMessageDeduplicator(5 * time.Minute) func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator { d := &MessageDeduplicator{ recentMessageTimes: make(map[string]time.Time), @@ -170,10 +160,7 @@ type PeerRateLimiter struct { mutex sync.Mutex } -// NewPeerRateLimiter creates a token bucket seeded with maxTokens and refilled -// at refillRate tokens per second. -// -// rateLimiter := NewPeerRateLimiter(100, 50) +// rateLimiter := NewPeerRateLimiter(100, 50) func NewPeerRateLimiter(maxTokens, refillPerSecond int) *PeerRateLimiter { return &PeerRateLimiter{ availableTokens: maxTokens, @@ -979,9 +966,7 @@ func (pc *PeerConnection) Close() error { return err } -// DisconnectPayload contains reason for disconnect. -// -// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} +// payload := DisconnectPayload{Reason: "shutdown", Code: DisconnectNormal} type DisconnectPayload struct { Reason string `json:"reason"` Code int `json:"code"` // Optional disconnect code From 1ee54add395bd882c8a0c3fe7745f65952d7bb2f Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:11:17 +0000 Subject: [PATCH 09/12] refactor(node): align remaining AX naming and examples Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/routing.md | 2 +- docs/ueps.md | 2 +- node/dispatcher.go | 11 +++++------ node/dispatcher_test.go | 12 ++++++------ node/integration_test.go | 2 +- node/levin/header.go | 12 ++++++------ node/message.go | 24 +++++++++--------------- node/protocol.go | 8 ++------ node/transport.go | 20 ++++++++++---------- node/worker.go | 6 ++---- 11 files changed, 44 insertions(+), 57 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 1f9f3a2..bb4b90c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -179,7 +179,7 @@ Auto-connect: if the target peer is not yet connected, `sendRequest` calls `tran |----------|-------|---------| | `IntentHandshake` | `0x01` | Connection establishment | | `IntentCompute` | `0x20` | Compute job request | -| `IntentRehab` | `0x30` | Benevolent intervention (pause execution) | +| `IntentPauseExecution` | `0x30` | Benevolent intervention (pause execution) | | `IntentCustom` | `0xFF` | Application-level sub-protocols | **Sentinel errors**: diff --git a/docs/routing.md b/docs/routing.md index 2bb3405..3881f01 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -84,7 +84,7 @@ Dropped packets are logged at WARN level with the threat score, threshold, inten const ( IntentHandshake byte = 0x01 // Connection establishment / hello IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) + IntentPauseExecution byte = 0x30 IntentCustom byte = 0xFF // Extended / application-level sub-protocols ) ``` diff --git a/docs/ueps.md b/docs/ueps.md index 09c0b71..24ab498 100644 --- a/docs/ueps.md +++ b/docs/ueps.md @@ -156,7 +156,7 @@ Reserved intent values: |----|----------|---------| | `0x01` | `IntentHandshake` | Connection establishment / hello | | `0x20` | `IntentCompute` | Compute job request | -| `0x30` | `IntentRehab` | Benevolent intervention (pause execution) | +| `0x30` | `IntentPauseExecution` | Benevolent intervention (pause execution) | | `0xFF` | `IntentCustom` | Extended / application-level sub-protocols | ## Threat Score diff --git a/node/dispatcher.go b/node/dispatcher.go index faedf4b..53e5cff 100644 --- a/node/dispatcher.go +++ b/node/dispatcher.go @@ -13,13 +13,12 @@ import ( // threshold := ThreatScoreThreshold const ThreatScoreThreshold uint16 = 50000 -// Well-known intent identifiers. These correspond to the semantic tokens -// carried in the UEPS IntentID header field (RFC-021). +// intentID := IntentPauseExecution const ( - IntentHandshake byte = 0x01 // Connection establishment / hello - IntentCompute byte = 0x20 // Compute job request - IntentRehab byte = 0x30 // Benevolent intervention (pause execution) - IntentCustom byte = 0xFF // Extended / application-level sub-protocols + IntentHandshake byte = 0x01 + IntentCompute byte = 0x20 + IntentPauseExecution byte = 0x30 + IntentCustom byte = 0xFF ) // var handler IntentHandler = func(packet *ueps.ParsedPacket) error { return nil } diff --git a/node/dispatcher_test.go b/node/dispatcher_test.go index 1cbe6e3..d458875 100644 --- a/node/dispatcher_test.go +++ b/node/dispatcher_test.go @@ -136,7 +136,7 @@ func TestDispatcher_UnknownIntentDropped_Bad(t *testing.T) { func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { d := NewDispatcher() - var handshakeCalled, computeCalled, rehabCalled, customCalled bool + var handshakeCalled, computeCalled, pauseExecutionCalled, customCalled bool d.RegisterHandler(IntentHandshake, func(pkt *ueps.ParsedPacket) error { handshakeCalled = true @@ -146,8 +146,8 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { computeCalled = true return nil }) - d.RegisterHandler(IntentRehab, func(pkt *ueps.ParsedPacket) error { - rehabCalled = true + d.RegisterHandler(IntentPauseExecution, func(pkt *ueps.ParsedPacket) error { + pauseExecutionCalled = true return nil }) d.RegisterHandler(IntentCustom, func(pkt *ueps.ParsedPacket) error { @@ -162,7 +162,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { }{ {"handshake routes correctly", IntentHandshake, &handshakeCalled}, {"compute routes correctly", IntentCompute, &computeCalled}, - {"rehab routes correctly", IntentRehab, &rehabCalled}, + {"pause execution routes correctly", IntentPauseExecution, &pauseExecutionCalled}, {"custom routes correctly", IntentCustom, &customCalled}, } @@ -171,7 +171,7 @@ func TestDispatcher_MultipleHandlersCorrectRouting_Good(t *testing.T) { // Reset all flags handshakeCalled = false computeCalled = false - rehabCalled = false + pauseExecutionCalled = false customCalled = false pkt := makePacket(tt.intentID, 0, []byte("payload")) @@ -341,6 +341,6 @@ func TestDispatcher_IntentConstants_Good(t *testing.T) { // Verify the well-known intent IDs match the spec (RFC-021). assert.Equal(t, byte(0x01), IntentHandshake) assert.Equal(t, byte(0x20), IntentCompute) - assert.Equal(t, byte(0x30), IntentRehab) + assert.Equal(t, byte(0x30), IntentPauseExecution) assert.Equal(t, byte(0xFF), IntentCustom) } diff --git a/node/integration_test.go b/node/integration_test.go index bd45880..13d40bc 100644 --- a/node/integration_test.go +++ b/node/integration_test.go @@ -572,7 +572,7 @@ func TestIntegration_DispatcherWithRealUEPSPackets_Good(t *testing.T) { }{ {IntentHandshake, "handshake", "hello"}, {IntentCompute, "compute", `{"job":"123"}`}, - {IntentRehab, "rehab", "pause"}, + {IntentPauseExecution, "pause-execution", "pause"}, {IntentCustom, "custom", "app-specific-data"}, } diff --git a/node/levin/header.go b/node/levin/header.go index 911435a..9da0760 100644 --- a/node/levin/header.go +++ b/node/levin/header.go @@ -11,25 +11,25 @@ import ( core "dappco.re/go/core" ) -// HeaderSize is the exact byte length of a serialised Levin header. +// headerBytes := make([]byte, HeaderSize) const HeaderSize = 33 -// Signature is the magic value that opens every Levin packet. +// header.Signature = Signature const Signature uint64 = 0x0101010101012101 -// MaxPayloadSize is the upper bound we accept for a single payload (100 MB). +// header.PayloadSize <= MaxPayloadSize const MaxPayloadSize uint64 = 100 * 1024 * 1024 -// Return-code constants carried in every Levin response. const ( + // returnCode := ReturnOK ReturnOK int32 = 0 ReturnErrorConnection int32 = -1 ReturnErrorFormat int32 = -7 ReturnErrorSignature int32 = -13 ) -// Command IDs for the CryptoNote P2P layer. const ( + // commandID := CommandHandshake CommandHandshake uint32 = 1001 CommandTimedSync uint32 = 1002 CommandPing uint32 = 1003 @@ -41,8 +41,8 @@ const ( CommandResponseChain uint32 = 2007 ) -// Sentinel errors returned by DecodeHeader. var ( + // err := ErrorBadSignature ErrorBadSignature = core.E("levin", "bad signature", nil) ErrorPayloadTooBig = core.E("levin", "payload exceeds maximum size", nil) ) diff --git a/node/message.go b/node/message.go index eb21c41..7fc85e4 100644 --- a/node/message.go +++ b/node/message.go @@ -8,18 +8,14 @@ import ( "github.com/google/uuid" ) -// Protocol version constants. const ( - // ProtocolVersion is the current protocol version. + // version := ProtocolVersion ProtocolVersion = "1.0" - // MinProtocolVersion is the minimum supported version. + // minimumVersion := MinProtocolVersion MinProtocolVersion = "1.0" ) -// SupportedProtocolVersions lists all protocol versions this node supports. -// Used for version negotiation during handshake. -// -// versions := SupportedProtocolVersions +// versions := SupportedProtocolVersions var SupportedProtocolVersions = []string{"1.0"} // payload := RawMessage(`{"pool":"pool.example.com:3333"}`) @@ -50,9 +46,7 @@ func IsProtocolVersionSupported(version string) bool { return slices.Contains(SupportedProtocolVersions, version) } -// MessageType defines the type of P2P message. -// -// messageType := MessagePing +// messageType := MessagePing type MessageType string const ( @@ -241,12 +235,12 @@ type ErrorPayload struct { Details string `json:"details,omitempty"` } -// Common error codes. const ( - ErrorCodeUnknown = 1000 - ErrorCodeInvalidMessage = 1001 - ErrorCodeUnauthorized = 1002 - ErrorCodeNotFound = 1003 + ErrorCodeUnknown = 1000 + ErrorCodeInvalidMessage = 1001 + ErrorCodeUnauthorized = 1002 + ErrorCodeNotFound = 1003 + // code := ErrorCodeOperationFailed ErrorCodeOperationFailed = 1004 ErrorCodeTimeout = 1005 ) diff --git a/node/protocol.go b/node/protocol.go index 23d5152..0565e76 100644 --- a/node/protocol.go +++ b/node/protocol.go @@ -58,16 +58,12 @@ func (h *ResponseHandler) ParseResponse(resp *Message, expectedType MessageType, // handler := DefaultResponseHandler var DefaultResponseHandler = &ResponseHandler{} -// ValidateResponse is a convenience function using the default handler. -// -// err := ValidateResponse(message, MessageStats) +// err := ValidateResponse(message, MessageStats) func ValidateResponse(resp *Message, expectedType MessageType) error { return DefaultResponseHandler.ValidateResponse(resp, expectedType) } -// ParseResponse is a convenience function using the default handler. -// -// err := ParseResponse(message, MessageStats, &stats) +// err := ParseResponse(message, MessageStats, &stats) func ParseResponse(resp *Message, expectedType MessageType, target any) error { return DefaultResponseHandler.ParseResponse(resp, expectedType, target) } diff --git a/node/transport.go b/node/transport.go index e2c74e9..f537787 100644 --- a/node/transport.go +++ b/node/transport.go @@ -27,10 +27,10 @@ var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). const messageLogSampleInterval = 100 -// DefaultMaxMessageSize is the default maximum message size (1MB) +// limit := DefaultMaxMessageSize const DefaultMaxMessageSize int64 = 1 << 20 // 1MB -// agentUserAgentPrefix identifies this tool in request headers. +// prefix := agentUserAgentPrefix const agentUserAgentPrefix = "agent-go-p2p" const ( @@ -41,14 +41,14 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // ":9091" default - WebSocketPath string // "/ws" - WebSocket endpoint path - TLSCertPath string // Optional TLS for wss:// - TLSKeyPath string - MaxConnections int // Maximum concurrent connections - MaxMessageSize int64 // Maximum message size in bytes (0 = 1MB default) - PingInterval time.Duration // WebSocket keepalive interval - PongTimeout time.Duration // Timeout waiting for pong + ListenAddr string // config.ListenAddr = ":9091" + WebSocketPath string // config.WebSocketPath = "/ws" + TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" + TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" + MaxConnections int // config.MaxConnections = 100 + MaxMessageSize int64 // config.MaxMessageSize = 1 << 20 + PingInterval time.Duration // config.PingInterval = 30 * time.Second + PongTimeout time.Duration // config.PongTimeout = 10 * time.Second } // transportConfig := DefaultTransportConfig() diff --git a/node/worker.go b/node/worker.go index 3de7277..073cdfc 100644 --- a/node/worker.go +++ b/node/worker.go @@ -39,7 +39,7 @@ type Worker struct { minerManager MinerManager profileManager ProfileManager startedAt time.Time - DeploymentDirectory string // Base directory for deployments (defaults to xdg.DataHome) + DeploymentDirectory string // worker.DeploymentDirectory = "/srv/p2p/deployments" } // worker := NewWorker(nodeManager, transport) @@ -395,9 +395,7 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// RegisterOnTransport installs the worker message handler on the transport. -// -// worker.RegisterOnTransport() +// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } From e5953e4b861caa4f5a88aea8fd3e092f82fdb18d Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:19:03 +0000 Subject: [PATCH 10/12] refactor(node): align AX comments and peer copy semantics Co-Authored-By: Virgil --- docs/architecture.md | 10 ++++----- docs/discovery.md | 8 +++---- docs/history.md | 6 ++--- docs/identity.md | 14 ++++++------ docs/transport.md | 12 +++++----- node/bundle.go | 8 ++----- node/identity.go | 5 ++--- node/peer.go | 33 +++++++++++++++++++++++----- node/peer_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ node/transport.go | 33 +++++++++++++++++++++++----- node/transport_test.go | 11 ++++++++++ 11 files changed, 146 insertions(+), 44 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index bb4b90c..8771504 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Keepalive**: A goroutine per connection ticks at `PingInterval`. If `LastActivity` has not been updated within `PingInterval + PongTimeout`, the connection is removed. -**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMu` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. +**Graceful close**: `GracefulClose` sends `MsgDisconnect` before closing the underlying WebSocket. Write deadlines are managed exclusively inside `Send()` under `writeMutex` to prevent the race (P2P-RACE-1) where a bare `SetWriteDeadline` call could race with concurrent sends. **Buffer pool**: `MarshalJSON` uses a `sync.Pool` of `bytes.Buffer` (initial capacity 1 KB, maximum pooled size 64 KB) to reduce allocation pressure in the message serialisation hot path. HTML escaping is disabled to match `json.Marshal` semantics. @@ -70,15 +70,15 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn **Peer fields persisted**: - `ID`, `Name`, `PublicKey`, `Address`, `Role`, `AddedAt`, `LastSeen` -- `PingMS`, `Hops`, `GeoKM`, `Score` (float64, 0–100) +- `PingMilliseconds`, `Hops`, `GeographicKilometres`, `Score` (float64, 0–100) **KD-tree dimensions** (lower is better in all axes): | Dimension | Weight | Rationale | |-----------|--------|-----------| -| `PingMS` | 1.0 | Latency dominates interactive performance | +| `PingMilliseconds` | 1.0 | Latency dominates interactive performance | | `Hops` | 0.7 | Network hop count (routing cost) | -| `GeoKM` | 0.2 | Geographic distance (minor factor) | +| `GeographicKilometres` | 0.2 | Geographic distance (minor factor) | | `100 - Score` | 1.2 | Reliability (inverted so lower = better peer) | `SelectOptimalPeer()` queries the tree for the point nearest to the origin (ideal: zero latency, zero hops, zero distance, maximum score). `SelectNearestPeers(n)` returns the n best. @@ -236,7 +236,7 @@ A global logger instance is available via `logging.Debug(...)`, `logging.Info(.. |----------|------------| | `Transport.conns` | `sync.RWMutex` | | `Transport.handler` | `sync.RWMutex` | -| `PeerConnection` writes | `sync.Mutex` (`writeMu`) | +| `PeerConnection` writes | `sync.Mutex` (`writeMutex`) | | `PeerConnection` close | `sync.Once` (`closeOnce`) | | `PeerRegistry.peers` + KD-tree | `sync.RWMutex` | | `PeerRegistry.allowedPublicKeys` | separate `sync.RWMutex` | diff --git a/docs/discovery.md b/docs/discovery.md index 3423165..452bb94 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -20,9 +20,9 @@ type Peer struct { LastSeen time.Time `json:"lastSeen"` // Poindexter metrics (updated dynamically) - PingMS float64 `json:"pingMs"` // Latency in milliseconds + PingMilliseconds float64 `json:"pingMs"` // Latency in milliseconds Hops int `json:"hops"` // Network hop count - GeoKM float64 `json:"geoKm"` // Geographic distance in kilometres + GeographicKilometres float64 `json:"geoKm"` // Geographic distance in kilometres Score float64 `json:"score"` // Reliability score 0--100 Connected bool `json:"-"` // Not persisted @@ -83,9 +83,9 @@ The registry maintains a 4-dimensional KD-tree for optimal peer selection. Each | Dimension | Source | Weight | Direction | |-----------|--------|--------|-----------| -| Latency | `PingMS` | 1.0 | Lower is better | +| Latency | `PingMilliseconds` | 1.0 | Lower is better | | Hops | `Hops` | 0.7 | Lower is better | -| Geographic distance | `GeoKM` | 0.2 | Lower is better | +| Geographic distance | `GeographicKilometres` | 0.2 | Lower is better | | Reliability | `100 - Score` | 1.2 | Inverted so lower is better | The score dimension is inverted so that the "ideal peer" target point `[0, 0, 0, 0]` represents zero latency, zero hops, zero distance, and maximum reliability (score 100). diff --git a/docs/history.md b/docs/history.md index 5c42f56..5c42f07 100644 --- a/docs/history.md +++ b/docs/history.md @@ -31,7 +31,7 @@ Tests covered: - MaxConnections enforcement: 503 HTTP rejection when limit is reached - Keepalive timeout: connection cleaned up after `PingInterval + PongTimeout` elapses - Graceful close: `MsgDisconnect` sent before underlying WebSocket close -- Concurrent sends: no data races under `go test -race` (`writeMu` protects all writes) +- Concurrent sends: no data races under `go test -race` (`writeMutex` protects all writes) ### Phase 3 — Controller Tests @@ -104,9 +104,9 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMu`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. -Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMu`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. +Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. ## Wiki Corrections (19 February 2026) diff --git a/docs/identity.md b/docs/identity.md index b102683..05c1a58 100644 --- a/docs/identity.md +++ b/docs/identity.md @@ -39,13 +39,13 @@ Paths follow XDG base directories via `github.com/adrg/xdg`. The private key is ### Creating an Identity ```go -nm, err := node.NewNodeManager() +nodeManager, err := node.NewNodeManager() if err != nil { log.Fatal(err) } // Generate a new identity (persists key and config to disk) -err = nm.GenerateIdentity("eu-controller-01", node.RoleController) +err = nodeManager.GenerateIdentity("eu-controller-01", node.RoleController) ``` Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce the X25519 keypair. @@ -53,7 +53,7 @@ Internally this calls `stmf.GenerateKeyPair()` from the Borg library to produce ### Custom Paths (Testing) ```go -nm, err := node.NewNodeManagerFromPaths( +nodeManager, err := node.NewNodeManagerFromPaths( "/tmp/test/private.key", "/tmp/test/node.json", ) @@ -62,8 +62,8 @@ nm, err := node.NewNodeManagerFromPaths( ### Checking and Retrieving Identity ```go -if nm.HasIdentity() { - identity := nm.GetIdentity() // Returns a copy +if nodeManager.HasIdentity() { + identity := nodeManager.GetIdentity() // Returns a copy fmt.Println(identity.ID, identity.Name) } ``` @@ -73,7 +73,7 @@ if nm.HasIdentity() { ### Deriving Shared Secrets ```go -sharedSecret, err := nm.DeriveSharedSecret(peerPublicKeyBase64) +sharedSecret, err := nodeManager.DeriveSharedSecret(peerPublicKeyBase64) ``` This performs X25519 ECDH with the peer's public key and hashes the result with SHA-256, producing a 32-byte symmetric key. The same shared secret is derived independently by both sides (no secret is transmitted). @@ -81,7 +81,7 @@ This performs X25519 ECDH with the peer's public key and hashes the result with ### Deleting an Identity ```go -err := nm.Delete() // Removes key and config from disk, clears in-memory state +err := nodeManager.Delete() // Removes key and config from disk, clears in-memory state ``` ## Challenge-Response Authentication diff --git a/docs/transport.md b/docs/transport.md index 5aad542..07a25e6 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -25,7 +25,7 @@ type TransportConfig struct { Sensible defaults via `DefaultTransportConfig()`: ```go -cfg := node.DefaultTransportConfig() +transportConfig := node.DefaultTransportConfig() // ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -33,10 +33,10 @@ cfg := node.DefaultTransportConfig() ## Creating and Starting ```go -transport := node.NewTransport(nodeManager, peerRegistry, cfg) +transport := node.NewTransport(nodeManager, peerRegistry, transportConfig) // Set message handler before Start() to avoid races -transport.OnMessage(func(conn *node.PeerConnection, msg *node.Message) { +transport.OnMessage(func(peerConnection *node.PeerConnection, msg *node.Message) { // Handle incoming messages }) @@ -96,15 +96,15 @@ type PeerConnection struct { ### Sending Messages ```go -err := peerConn.Send(msg) +err := peerConnection.Send(msg) ``` -`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMu` mutex serialises concurrent writes. +`Send()` serialises the message to JSON, encrypts it with SMSG, sets a 10-second write deadline, and writes as a binary WebSocket frame. A `writeMutex` serialises concurrent writes. ### Graceful Close ```go -err := peerConn.GracefulClose("shutting down", node.DisconnectShutdown) +err := peerConnection.GracefulClose("shutting down", node.DisconnectShutdown) ``` Sends a `disconnect` message (best-effort) before closing the connection. Uses `sync.Once` to ensure the connection is only closed once. diff --git a/node/bundle.go b/node/bundle.go index f1c7342..30e409b 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -347,9 +347,7 @@ func extractTarball(tarData []byte, destDir string) (string, error) { return firstExecutable, nil } -// StreamBundle writes a bundle to a writer (for large transfers). -// -// err := StreamBundle(bundle, writer) +// err := StreamBundle(bundle, writer) func StreamBundle(bundle *Bundle, w io.Writer) error { result := core.JSONMarshal(bundle) if !result.OK { @@ -359,9 +357,7 @@ func StreamBundle(bundle *Bundle, w io.Writer) error { return err } -// ReadBundle reads a bundle from a reader. -// -// bundle, err := ReadBundle(reader) +// bundle, err := ReadBundle(reader) func ReadBundle(r io.Reader) (*Bundle, error) { var buf bytes.Buffer if _, err := io.Copy(&buf, r); err != nil { diff --git a/node/identity.go b/node/identity.go index ae5d1f1..7bb3a1d 100644 --- a/node/identity.go +++ b/node/identity.go @@ -87,10 +87,9 @@ func NewNodeManager() (*NodeManager, error) { return NewNodeManagerFromPaths(keyPath, configPath) } -// Missing files are treated as a fresh install; malformed or partial identity -// state is returned as an error so callers can handle it explicitly. -// // nodeManager, err := NewNodeManagerFromPaths("/srv/p2p/private.key", "/srv/p2p/node.json") +// Missing files are treated as a fresh install; malformed or partial identity +// state returns an error so callers can handle it explicitly. func NewNodeManagerFromPaths(keyPath, configPath string) (*NodeManager, error) { nm := &NodeManager{ keyPath: keyPath, diff --git a/node/peer.go b/node/peer.go index 42f8801..27a80d7 100644 --- a/node/peer.go +++ b/node/peer.go @@ -131,10 +131,9 @@ func NewPeerRegistry() (*PeerRegistry, error) { return NewPeerRegistryFromPath(peersPath) } -// Missing files are treated as an empty registry; malformed registry files are -// returned as errors so callers can repair the persisted state. -// // peerRegistry, err := NewPeerRegistryFromPath("/srv/p2p/peers.json") +// Missing files are treated as an empty registry; malformed registry files +// return an error so callers can repair the persisted state. func NewPeerRegistryFromPath(peersPath string) (*PeerRegistry, error) { pr := &PeerRegistry{ peers: make(map[string]*Peer), @@ -248,6 +247,13 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { // // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.AddPeer", "peer is nil", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if peer.ID == "" { @@ -271,7 +277,7 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { peer.AddedAt = time.Now() } if peer.Score == 0 { - peer.Score = 50 // Default neutral score + peer.Score = ScoreDefault } r.peers[peer.ID] = peer @@ -286,6 +292,17 @@ func (r *PeerRegistry) AddPeer(peer *Peer) error { // // err := registry.UpdatePeer(&Peer{ID: "worker-1", Score: 90}) func (r *PeerRegistry) UpdatePeer(peer *Peer) error { + if peer == nil { + return core.E("PeerRegistry.UpdatePeer", "peer is nil", nil) + } + + if peer.ID == "" { + return core.E("PeerRegistry.UpdatePeer", "peer ID is required", nil) + } + + peerCopy := *peer + peer = &peerCopy + r.mu.Lock() if _, exists := r.peers[peer.ID]; !exists { @@ -502,7 +519,13 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { return 0 }) - return peers + peerCopies := make([]*Peer, 0, len(peers)) + for _, peer := range peers { + peerCopy := *peer + peerCopies = append(peerCopies, &peerCopy) + } + + return peerCopies } // for peer := range registry.PeersByScore() { diff --git a/node/peer_test.go b/node/peer_test.go index b10979c..45d6f0f 100644 --- a/node/peer_test.go +++ b/node/peer_test.go @@ -65,6 +65,15 @@ func TestPeer_Registry_AddPeer_Good(t *testing.T) { t.Errorf("expected 1 peer, got %d", pr.Count()) } + peer.Name = "Mutated after add" + stored := pr.GetPeer("test-peer-1") + if stored == nil { + t.Fatal("expected peer to exist after add") + } + if stored.Name != "Test Peer" { + t.Errorf("expected stored peer to remain unchanged, got %q", stored.Name) + } + // Try to add duplicate err = pr.AddPeer(peer) if err == nil { @@ -634,6 +643,34 @@ func TestPeer_Registry_PeersSortedByScore_Good(t *testing.T) { if sorted[2].ID != "low-score" { t.Errorf("third peer should be low-score, got %s", sorted[2].ID) } + + sorted[0].Name = "Mutated" + restored := pr.GetPeer("high-score") + if restored == nil { + t.Fatal("expected high-score peer to still exist") + } + if restored.Name != "High" { + t.Errorf("expected registry peer to remain unchanged, got %q", restored.Name) + } +} + +func TestPeer_Registry_NilPeerInputs_Bad(t *testing.T) { + pr, cleanup := setupTestPeerRegistry(t) + defer cleanup() + + t.Run("AddPeer", func(t *testing.T) { + err := pr.AddPeer(nil) + if err == nil { + t.Fatal("expected error when adding nil peer") + } + }) + + t.Run("UpdatePeer", func(t *testing.T) { + err := pr.UpdatePeer(nil) + if err == nil { + t.Fatal("expected error when updating nil peer") + } + }) } // --- Additional coverage tests for peer.go --- @@ -736,6 +773,19 @@ func TestPeer_Registry_UpdatePeer_Good(t *testing.T) { if updated.Score != 80 { t.Errorf("expected score 80, got %f", updated.Score) } + + peer.Name = "Mutated after update" + peer.Score = 12 + stored := pr.GetPeer("update-test") + if stored == nil { + t.Fatal("expected peer to exist after update mutation") + } + if stored.Name != "Updated" { + t.Errorf("expected stored peer name to remain Updated, got %q", stored.Name) + } + if stored.Score != 80 { + t.Errorf("expected stored peer score to remain 80, got %f", stored.Score) + } } func TestPeer_Registry_UpdateMetrics_NotFound_Bad(t *testing.T) { diff --git a/node/transport.go b/node/transport.go index f537787..9c93165 100644 --- a/node/transport.go +++ b/node/transport.go @@ -107,22 +107,45 @@ func NewMessageDeduplicator(retentionWindow time.Duration) *MessageDeduplicator return d } -// IsDuplicate checks whether a message ID is still within the retention window. +// duplicate := deduplicator.IsDuplicate(message.ID) func (d *MessageDeduplicator) IsDuplicate(msgID string) bool { d.mutex.RLock() - _, exists := d.recentMessageTimes[msgID] + seenAt, exists := d.recentMessageTimes[msgID] + retentionWindow := d.timeToLive d.mutex.RUnlock() - return exists + + if !exists { + return false + } + + if retentionWindow > 0 && time.Since(seenAt) <= retentionWindow { + return true + } + + d.mutex.Lock() + defer d.mutex.Unlock() + + seenAt, exists = d.recentMessageTimes[msgID] + if !exists { + return false + } + + if retentionWindow <= 0 || time.Since(seenAt) > retentionWindow { + delete(d.recentMessageTimes, msgID) + return false + } + + return true } -// Mark records a message ID as recently seen. +// deduplicator.Mark(message.ID) func (d *MessageDeduplicator) Mark(msgID string) { d.mutex.Lock() d.recentMessageTimes[msgID] = time.Now() d.mutex.Unlock() } -// Cleanup removes expired entries from the deduplicator. +// deduplicator.Cleanup() func (d *MessageDeduplicator) Cleanup() { d.mutex.Lock() defer d.mutex.Unlock() diff --git a/node/transport_test.go b/node/transport_test.go index 24969b1..16acd46 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -153,6 +153,17 @@ func TestTransport_MessageDeduplicator_Good(t *testing.T) { } }) + t.Run("ExpiredEntriesDoNotLinger", func(t *testing.T) { + d := NewMessageDeduplicator(50 * time.Millisecond) + d.Mark("msg-1") + + time.Sleep(75 * time.Millisecond) + + if d.IsDuplicate("msg-1") { + t.Error("should not be duplicate after TTL even before cleanup runs") + } + }) + t.Run("ConcurrentAccess", func(t *testing.T) { d := NewMessageDeduplicator(5 * time.Minute) var wg sync.WaitGroup From 849a716360b344b4d977e9e4e955388c2b3ed5d8 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:25:37 +0000 Subject: [PATCH 11/12] refactor(node): trim redundant AX comments Co-Authored-By: Virgil --- node/buffer_pool.go | 2 -- node/bundle.go | 20 -------------------- node/controller.go | 12 ------------ node/errors.go | 2 -- node/identity.go | 9 --------- node/levin/connection.go | 2 -- node/peer.go | 6 ------ node/worker.go | 23 ----------------------- 8 files changed, 76 deletions(-) diff --git a/node/buffer_pool.go b/node/buffer_pool.go index 91f160d..b883e09 100644 --- a/node/buffer_pool.go +++ b/node/buffer_pool.go @@ -15,14 +15,12 @@ var bufferPool = sync.Pool{ }, } -// getBuffer retrieves a buffer from the pool. func getBuffer() *bytes.Buffer { buffer := bufferPool.Get().(*bytes.Buffer) buffer.Reset() return buffer } -// putBuffer returns a buffer to the pool. func putBuffer(buffer *bytes.Buffer) { // Don't pool buffers that grew too large (>64KB) if buffer.Cap() <= 65536 { diff --git a/node/bundle.go b/node/bundle.go index 30e409b..034fd95 100644 --- a/node/bundle.go +++ b/node/bundle.go @@ -46,20 +46,17 @@ type BundleManifest struct { // bundle, err := CreateProfileBundle(profileJSON, "xmrig-default", "password") func CreateProfileBundle(profileJSON []byte, name string, password string) (*Bundle, error) { - // Create a TIM with just the profile config timBundle, err := tim.New() if err != nil { return nil, core.E("CreateProfileBundle", "failed to create TIM", err) } timBundle.Config = profileJSON - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateProfileBundle", "failed to encrypt bundle", err) } - // Calculate checksum checksum := calculateChecksum(stimData) return &Bundle{ @@ -84,14 +81,12 @@ func CreateProfileBundleUnencrypted(profileJSON []byte, name string) (*Bundle, e // bundle, err := CreateMinerBundle("/srv/miners/xmrig", profileJSON, "xmrig", "password") func CreateMinerBundle(minerPath string, profileJSON []byte, name string, password string) (*Bundle, error) { - // Read miner binary minerContent, err := filesystemRead(minerPath) if err != nil { return nil, core.E("CreateMinerBundle", "failed to read miner binary", err) } minerData := []byte(minerContent) - // Create a tarball with the miner binary tarData, err := createTarball(map[string][]byte{ core.PathBase(minerPath): minerData, }) @@ -99,24 +94,20 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo return nil, core.E("CreateMinerBundle", "failed to create tarball", err) } - // Create DataNode from tarball dataNode, err := datanode.FromTar(tarData) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create datanode", err) } - // Create TIM from DataNode timBundle, err := tim.FromDataNode(dataNode) if err != nil { return nil, core.E("CreateMinerBundle", "failed to create TIM", err) } - // Set profile as config if provided if profileJSON != nil { timBundle.Config = profileJSON } - // Encrypt to STIM format stimData, err := timBundle.ToSigil(password) if err != nil { return nil, core.E("CreateMinerBundle", "failed to encrypt bundle", err) @@ -134,17 +125,14 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo // profileJSON, err := ExtractProfileBundle(bundle, "password") func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { - // Verify checksum first if calculateChecksum(bundle.Data) != bundle.Checksum { return nil, core.E("ExtractProfileBundle", "checksum mismatch - bundle may be corrupted", nil) } - // If it's unencrypted JSON, just return it if isJSON(bundle.Data) { return bundle.Data, nil } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return nil, core.E("ExtractProfileBundle", "failed to decrypt bundle", err) @@ -155,24 +143,20 @@ func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { // minerPath, profileJSON, err := ExtractMinerBundle(bundle, "password", "/srv/miners") func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string, []byte, error) { - // Verify checksum if calculateChecksum(bundle.Data) != bundle.Checksum { return "", nil, core.E("ExtractMinerBundle", "checksum mismatch - bundle may be corrupted", nil) } - // Decrypt STIM format timBundle, err := tim.FromSigil(bundle.Data, password) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to decrypt bundle", err) } - // Convert rootfs to tarball and extract tarData, err := timBundle.RootFS.ToTar() if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to convert rootfs to tar", err) } - // Extract tarball to destination minerPath, err := extractTarball(tarData, destDir) if err != nil { return "", nil, core.E("ExtractMinerBundle", "failed to extract tarball", err) @@ -186,13 +170,11 @@ func VerifyBundle(bundle *Bundle) bool { return calculateChecksum(bundle.Data) == bundle.Checksum } -// calculateChecksum computes SHA-256 checksum of data. func calculateChecksum(data []byte) string { hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } -// isJSON checks if data starts with JSON characters. func isJSON(data []byte) bool { if len(data) == 0 { return false @@ -201,7 +183,6 @@ func isJSON(data []byte) bool { return data[0] == '{' || data[0] == '[' } -// createTarball creates a tar archive from a map of filename -> content. func createTarball(files map[string][]byte) ([]byte, error) { var buf bytes.Buffer tarWriter := tar.NewWriter(&buf) @@ -248,7 +229,6 @@ func createTarball(files map[string][]byte) ([]byte, error) { return buf.Bytes(), nil } -// extractTarball extracts a tar archive to a directory, returns first executable found. func extractTarball(tarData []byte, destDir string) (string, error) { // Ensure destDir is an absolute, clean path for security checks absDestDir := destDir diff --git a/node/controller.go b/node/controller.go index 17bbc22..8441352 100644 --- a/node/controller.go +++ b/node/controller.go @@ -36,7 +36,6 @@ func NewController(nodeManager *NodeManager, peerRegistry *PeerRegistry, transpo return c } -// handleResponse processes incoming replies and routes them to the waiting request. func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { if message.ReplyTo == "" { return // Not a response, let worker handle it @@ -58,12 +57,6 @@ func (c *Controller) handleResponse(_ *PeerConnection, message *Message) { } } -// sendRequest registers a temporary response channel, sends message, and waits -// for the matching reply or timeout. -// -// The response channel is intentionally never closed. Removing it from the -// pending map is enough to stop future routing, and it avoids a late-response -// close/send race after the caller has already timed out. func (c *Controller) sendRequest(peerID string, message *Message, timeout time.Duration) (*Message, error) { resolvedPeerID := peerID @@ -77,13 +70,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D if err != nil { return nil, core.E("Controller.sendRequest", "failed to connect to peer", err) } - // Use the real peer ID after handshake (it may have changed) resolvedPeerID = conn.Peer.ID - // Update the message destination message.To = resolvedPeerID } - // Create response channel responseChannel := make(chan *Message, 1) c.mutex.Lock() @@ -98,12 +88,10 @@ func (c *Controller) sendRequest(peerID string, message *Message, timeout time.D c.mutex.Unlock() }() - // Send the message if err := c.transport.Send(resolvedPeerID, message); err != nil { return nil, core.E("Controller.sendRequest", "failed to send message", err) } - // Wait for response ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() diff --git a/node/errors.go b/node/errors.go index 302a34a..26ac3d6 100644 --- a/node/errors.go +++ b/node/errors.go @@ -3,9 +3,7 @@ package node import core "dappco.re/go/core" var ( - // err := ErrorIdentityNotInitialized ErrorIdentityNotInitialized = core.E("node", "node identity not initialized", nil) - // err := ErrorMinerManagerNotConfigured ErrorMinerManagerNotConfigured = core.E("node", "miner manager not configured", nil) ) diff --git a/node/identity.go b/node/identity.go index 7bb3a1d..9679305 100644 --- a/node/identity.go +++ b/node/identity.go @@ -199,15 +199,12 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error return hash[:], nil } -// savePrivateKey saves the private key to disk with restricted permissions. func (n *NodeManager) savePrivateKey() error { - // Ensure directory exists dir := core.PathDir(n.keyPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.savePrivateKey", "failed to create key directory", err) } - // Write private key if err := filesystemWrite(n.keyPath, string(n.privateKey)); err != nil { return core.E("NodeManager.savePrivateKey", "failed to write private key", err) } @@ -215,9 +212,7 @@ func (n *NodeManager) savePrivateKey() error { return nil } -// saveIdentity saves the public identity to the config file. func (n *NodeManager) saveIdentity() error { - // Ensure directory exists dir := core.PathDir(n.configPath) if err := filesystemEnsureDir(dir); err != nil { return core.E("NodeManager.saveIdentity", "failed to create config directory", err) @@ -236,9 +231,7 @@ func (n *NodeManager) saveIdentity() error { return nil } -// loadIdentity loads the node identity from disk. func (n *NodeManager) loadIdentity() error { - // Load identity config content, err := filesystemRead(n.configPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read identity", err) @@ -250,14 +243,12 @@ func (n *NodeManager) loadIdentity() error { return core.E("NodeManager.loadIdentity", "failed to unmarshal identity", result.Value.(error)) } - // Load private key keyContent, err := filesystemRead(n.keyPath) if err != nil { return core.E("NodeManager.loadIdentity", "failed to read private key", err) } privateKey := []byte(keyContent) - // Reconstruct keypair from private key keyPair, err := stmf.LoadKeyPair(privateKey) if err != nil { return core.E("NodeManager.loadIdentity", "failed to load keypair", err) diff --git a/node/levin/connection.go b/node/levin/connection.go index 76bd643..a479b04 100644 --- a/node/levin/connection.go +++ b/node/levin/connection.go @@ -79,7 +79,6 @@ func (connection *Connection) WriteResponse(commandID uint32, payload []byte, re return connection.writeFrame(&header, payload) } -// writeFrame serialises header + payload and writes them atomically. func (connection *Connection) writeFrame(header *Header, payload []byte) error { headerBytes := EncodeHeader(header) @@ -109,7 +108,6 @@ func (connection *Connection) ReadPacket() (Header, []byte, error) { return Header{}, nil, err } - // Read header. var headerBytes [HeaderSize]byte if _, err := io.ReadFull(connection.networkConnection, headerBytes[:]); err != nil { return Header{}, nil, err diff --git a/node/peer.go b/node/peer.go index 27a80d7..55e9fa6 100644 --- a/node/peer.go +++ b/node/peer.go @@ -64,7 +64,6 @@ const ( // peerNamePattern validates peer names: alphanumeric, hyphens, underscores, and spaces. var peerNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-_ ]{0,62}[a-zA-Z0-9]$|^[a-zA-Z0-9]$`) -// safeKeyPrefix returns a truncated key for logging, handling short keys safely func safeKeyPrefix(key string) string { if len(key) >= 16 { return key[:16] + "..." @@ -75,9 +74,6 @@ func safeKeyPrefix(key string) string { return key } -// validatePeerName checks if a peer name is valid. -// Peer names must be 1-64 characters, start and end with alphanumeric, -// and contain only alphanumeric, hyphens, underscores, and spaces. func validatePeerName(name string) error { if name == "" { return nil // Empty names are allowed (optional field) @@ -347,7 +343,6 @@ func (r *PeerRegistry) GetPeer(id string) *Peer { return nil } - // Return a copy peerCopy := *peer return &peerCopy } @@ -508,7 +503,6 @@ func (r *PeerRegistry) GetPeersByScore() []*Peer { peers := slices.Collect(maps.Values(r.peers)) - // Sort by score descending slices.SortFunc(peers, func(a, b *Peer) int { if b.Score > a.Score { return 1 diff --git a/node/worker.go b/node/worker.go index 073cdfc..03875f2 100644 --- a/node/worker.go +++ b/node/worker.go @@ -111,7 +111,6 @@ func (w *Worker) HandleMessage(peerConnection *PeerConnection, message *Message) } } -// handlePing responds to ping requests. func (w *Worker) handlePing(message *Message) (*Message, error) { var ping PingPayload if err := message.ParsePayload(&ping); err != nil { @@ -126,7 +125,6 @@ func (w *Worker) handlePing(message *Message) (*Message, error) { return message.Reply(MessagePong, pong) } -// handleStats responds with current miner statistics. func (w *Worker) handleStats(message *Message) (*Message, error) { identity := w.nodeManager.GetIdentity() if identity == nil { @@ -148,8 +146,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { continue } - // Convert to MinerStatsItem - this is a simplified conversion - // The actual implementation would need to match the mining package's stats structure item := convertMinerStats(miner, minerStats) stats.Miners = append(stats.Miners, item) } @@ -158,7 +154,6 @@ func (w *Worker) handleStats(message *Message) (*Message, error) { return message.Reply(MessageStats, stats) } -// convertMinerStats converts a running miner's raw stats map to the wire protocol format. func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { item := MinerStatsItem{ Name: miner.GetName(), @@ -189,7 +184,6 @@ func convertMinerStats(miner MinerInstance, rawStats any) MinerStatsItem { return item } -// handleStartMiner starts a miner with the given profile. func (w *Worker) handleStartMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -200,12 +194,10 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "invalid start miner payload", err) } - // Validate miner type is provided if payload.MinerType == "" { return nil, core.E("Worker.handleStartMiner", "miner type is required", nil) } - // Get the config from the profile or use the override var config any if payload.Config != nil { config = payload.Config @@ -219,7 +211,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return nil, core.E("Worker.handleStartMiner", "no config provided and no profile manager configured", nil) } - // Start the miner miner, err := w.minerManager.StartMiner(payload.MinerType, config) if err != nil { ack := MinerAckPayload{ @@ -236,7 +227,6 @@ func (w *Worker) handleStartMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleStopMiner stops a running miner. func (w *Worker) handleStopMiner(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -259,7 +249,6 @@ func (w *Worker) handleStopMiner(message *Message) (*Message, error) { return message.Reply(MessageMinerAck, ack) } -// handleLogs returns console logs from a miner. func (w *Worker) handleLogs(message *Message) (*Message, error) { if w.minerManager == nil { return nil, ErrorMinerManagerNotConfigured @@ -270,7 +259,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return nil, core.E("Worker.handleLogs", "invalid logs payload", err) } - // Validate and limit the Lines parameter to prevent resource exhaustion const maxLogLines = 10000 if payload.Lines <= 0 || payload.Lines > maxLogLines { payload.Lines = maxLogLines @@ -292,7 +280,6 @@ func (w *Worker) handleLogs(message *Message) (*Message, error) { return message.Reply(MessageLogs, logs) } -// handleDeploy handles deployment of profiles or miner bundles. func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) (*Message, error) { var payload DeployPayload if err := message.ParsePayload(&payload); err != nil { @@ -319,13 +306,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return nil, core.E("Worker.handleDeploy", "profile manager not configured", nil) } - // Decrypt and extract profile data profileData, err := ExtractProfileBundle(bundle, password) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract profile bundle", err) } - // Unmarshal into interface{} to pass to ProfileManager var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { return nil, core.E("Worker.handleDeploy", "invalid profile data JSON", result.Value.(error)) @@ -347,8 +332,6 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) return message.Reply(MessageDeployAck, ack) case BundleMiner, BundleFull: - // Determine the installation directory under the configured deployment - // root for lethean-desktop/miners/. minersDir := core.JoinPath(w.deploymentDirectory(), "lethean-desktop", "miners") installDir := core.JoinPath(minersDir, payload.Name) @@ -358,13 +341,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) "type": payload.BundleType, }) - // Extract miner bundle minerPath, profileData, err := ExtractMinerBundle(bundle, password, installDir) if err != nil { return nil, core.E("Worker.handleDeploy", "failed to extract miner bundle", err) } - // If the bundle contained a profile config, save it if len(profileData) > 0 && w.profileManager != nil { var profile any if result := core.JSONUnmarshal(profileData, &profile); !result.OK { @@ -376,13 +357,11 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } - // Success response ack := DeployAckPayload{ Success: true, Name: payload.Name, } - // Log the installation logging.Info("miner bundle installed successfully", logging.Fields{ "name": payload.Name, "miner_path": minerPath, @@ -395,12 +374,10 @@ func (w *Worker) handleDeploy(peerConnection *PeerConnection, message *Message) } } -// worker.RegisterOnTransport() func (w *Worker) RegisterOnTransport() { w.transport.OnMessage(w.HandleMessage) } -// deploymentDirectory resolves the active deployment directory. func (w *Worker) deploymentDirectory() string { if w.DeploymentDirectory != "" { return w.DeploymentDirectory From 2eecb6cfc695dabf68e4f8ff77f574045eb872d4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 14:32:55 +0000 Subject: [PATCH 12/12] refactor(node): align transport naming with AX Co-Authored-By: Virgil --- docs/architecture.md | 2 +- docs/history.md | 2 +- docs/transport.md | 8 +-- node/peer.go | 6 -- node/transport.go | 133 +++++++++++++++++++++++++---------------- node/transport_test.go | 4 +- specs/node.md | 6 +- 7 files changed, 93 insertions(+), 68 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8771504..dc204a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ The `Transport` manages a WebSocket server (gorilla/websocket) and outbound conn | Field | Default | Purpose | |-------|---------|---------| -| `ListenAddr` | `:9091` | HTTP bind address | +| `ListenAddress` | `:9091` | HTTP bind address | | `WebSocketPath` | `/ws` | WebSocket endpoint | | `MaxConnections` | 100 | Maximum concurrent connections | | `MaxMessageSize` | 1 MB | Read limit per message | diff --git a/docs/history.md b/docs/history.md index 5c42f07..986727f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -104,7 +104,7 @@ The originally identified risk — that `transport.OnMessage(c.handleResponse)` ### P2P-RACE-1 — GracefulClose Data Race (Phase 3) -`GracefulClose` previously called `pc.Conn.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. +`GracefulClose` previously called `pc.WebSocketConnection.SetWriteDeadline()` outside of `writeMutex`, racing with concurrent `Send()` calls that also set the write deadline. Detected by `go test -race`. Fix: removed the bare `SetWriteDeadline` call from `GracefulClose`. The method now relies entirely on `Send()`, which manages write deadlines under `writeMutex`. This is documented in a comment in `transport.go` to prevent the pattern from being reintroduced. diff --git a/docs/transport.md b/docs/transport.md index 07a25e6..56b55f9 100644 --- a/docs/transport.md +++ b/docs/transport.md @@ -11,7 +11,7 @@ The `Transport` manages encrypted WebSocket connections between nodes. After an ```go type TransportConfig struct { - ListenAddr string // ":9091" default + ListenAddress string // ":9091" default WebSocketPath string // "/ws" -- WebSocket endpoint path TLSCertPath string // Optional TLS for wss:// TLSKeyPath string @@ -26,7 +26,7 @@ Sensible defaults via `DefaultTransportConfig()`: ```go transportConfig := node.DefaultTransportConfig() -// ListenAddr: ":9091", WebSocketPath: "/ws", MaxConnections: 100 +// ListenAddress: ":9091", WebSocketPath: "/ws", MaxConnections: 100 // MaxMessageSize: 1MB, PingInterval: 30s, PongTimeout: 10s ``` @@ -86,8 +86,8 @@ Each active connection is wrapped in a `PeerConnection`: ```go type PeerConnection struct { - Peer *Peer // Remote peer identity - Conn *websocket.Conn // Underlying WebSocket + Peer *Peer // Remote peer identity + WebSocketConnection *websocket.Conn // Underlying WebSocket SharedSecret []byte // From X25519 ECDH LastActivity time.Time } diff --git a/node/peer.go b/node/peer.go index 55e9fa6..2b7606b 100644 --- a/node/peer.go +++ b/node/peer.go @@ -42,7 +42,6 @@ type Peer struct { Connected bool `json:"-"` } -// peerRegistrySaveDebounceInterval is the minimum time between disk writes. const peerRegistrySaveDebounceInterval = 5 * time.Second // mode := PeerAuthAllowlist @@ -239,8 +238,6 @@ func (r *PeerRegistry) AllowedPublicKeys() iter.Seq[string] { } } -// Persistence is debounced — writes are batched every 5s. Call Close() before shutdown. -// // err := registry.AddPeer(&Peer{ID: "worker-1", Address: "10.0.0.1:9091", Role: RoleWorker}) func (r *PeerRegistry) AddPeer(peer *Peer) error { if peer == nil { @@ -623,8 +620,6 @@ func (r *PeerRegistry) Count() int { return len(r.peers) } -// rebuildKDTree rebuilds the KD-tree from current peers. -// Must be called with lock held. func (r *PeerRegistry) rebuildKDTree() { if len(r.peers) == 0 { r.kdTree = nil @@ -746,7 +741,6 @@ func (r *PeerRegistry) Close() error { return nil } -// load reads peers from disk. func (r *PeerRegistry) load() error { content, err := filesystemRead(r.path) if err != nil { diff --git a/node/transport.go b/node/transport.go index 9c93165..6004aed 100644 --- a/node/transport.go +++ b/node/transport.go @@ -21,7 +21,6 @@ import ( "github.com/gorilla/websocket" ) -// messageLogSampleCounter tracks message counts for sampled debug logs. var messageLogSampleCounter atomic.Int64 // messageLogSampleInterval controls how often we log debug messages in hot paths (1 in N). @@ -41,7 +40,8 @@ const ( // transportConfig := DefaultTransportConfig() type TransportConfig struct { - ListenAddr string // config.ListenAddr = ":9091" + ListenAddress string // config.ListenAddress = ":9091" + ListenAddr string WebSocketPath string // config.WebSocketPath = "/ws" TLSCertPath string // config.TLSCertPath = "/srv/p2p/tls.crt" TLSKeyPath string // config.TLSKeyPath = "/srv/p2p/tls.key" @@ -54,6 +54,7 @@ type TransportConfig struct { // transportConfig := DefaultTransportConfig() func DefaultTransportConfig() TransportConfig { return TransportConfig{ + ListenAddress: defaultTransportListenAddress, ListenAddr: defaultTransportListenAddress, WebSocketPath: defaultTransportWebSocketPath, MaxConnections: defaultTransportMaximumConnections, @@ -63,16 +64,22 @@ func DefaultTransportConfig() TransportConfig { } } -// listenAddress returns the effective listen address, falling back to the -// default when the config leaves it empty. func (c TransportConfig) listenAddress() string { + if c.ListenAddress != "" && c.ListenAddress != defaultTransportListenAddress { + return c.ListenAddress + } + if c.ListenAddr != "" && c.ListenAddr != defaultTransportListenAddress { + return c.ListenAddr + } + if c.ListenAddress != "" { + return c.ListenAddress + } if c.ListenAddr != "" { return c.ListenAddr } return defaultTransportListenAddress } -// webSocketPath returns the effective WebSocket endpoint path. func (c TransportConfig) webSocketPath() string { if c.WebSocketPath != "" { return c.WebSocketPath @@ -80,7 +87,6 @@ func (c TransportConfig) webSocketPath() string { return defaultTransportWebSocketPath } -// maximumConnections returns the effective concurrent connection limit. func (c TransportConfig) maximumConnections() int { if c.MaxConnections > 0 { return c.MaxConnections @@ -217,15 +223,16 @@ func (r *PeerRateLimiter) Allow() bool { // peerConnection := &PeerConnection{Peer: &Peer{ID: "worker-1"}} type PeerConnection struct { - Peer *Peer - Conn *websocket.Conn - SharedSecret []byte // Derived via X25519 ECDH, used for SMSG - LastActivity time.Time - UserAgent string // Request identity advertised by the peer - writeMutex sync.Mutex // Serialize WebSocket writes - transport *Transport - closeOnce sync.Once // Ensure Close() is only called once - rateLimiter *PeerRateLimiter // Per-peer message rate limiting + Peer *Peer + WebSocketConnection *websocket.Conn + Conn *websocket.Conn + SharedSecret []byte // Derived via X25519 ECDH, used for SMSG + LastActivity time.Time + UserAgent string // Request identity advertised by the peer + writeMutex sync.Mutex // Serialize WebSocket writes + transport *Transport + closeOnce sync.Once // Ensure Close() is only called once + rateLimiter *PeerRateLimiter // Per-peer message rate limiting } // transport := NewTransport(nodeManager, peerRegistry, DefaultTransportConfig()) @@ -261,7 +268,13 @@ func NewTransport(nodeManager *NodeManager, peerRegistry *PeerRegistry, config T } } -// agentHeaderToken converts free-form identity data into a stable header token. +func (pc *PeerConnection) webSocketConnection() *websocket.Conn { + if pc.WebSocketConnection != nil { + return pc.WebSocketConnection + } + return pc.Conn +} + func agentHeaderToken(value string) string { value = strings.TrimSpace(value) if value == "" { @@ -295,7 +308,6 @@ func agentHeaderToken(value string) string { return token } -// agentUserAgent returns a transparent identity string for request headers. func (t *Transport) agentUserAgent() string { identity := t.nodeManager.GetIdentity() if identity == nil { @@ -437,12 +449,13 @@ func (t *Transport) Connect(peer *Peer) (*PeerConnection, error) { } peerConnection := &PeerConnection{ - Peer: peer, - Conn: conn, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } // Perform handshake with challenge-response authentication. @@ -531,7 +544,6 @@ func (t *Transport) GetConnection(peerID string) *PeerConnection { return t.connections[peerID] } -// handleWebSocketUpgrade handles incoming WebSocket connections. func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Request) { userAgent := r.Header.Get("User-Agent") @@ -668,13 +680,14 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques } pc := &PeerConnection{ - Peer: peer, - Conn: conn, - SharedSecret: sharedSecret, - LastActivity: time.Now(), - UserAgent: userAgent, - transport: t, - rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill + Peer: peer, + WebSocketConnection: conn, + Conn: conn, + SharedSecret: sharedSecret, + LastActivity: time.Now(), + UserAgent: userAgent, + transport: t, + rateLimiter: NewPeerRateLimiter(100, 50), // 100 burst, 50/sec refill } identity := t.nodeManager.GetIdentity() @@ -738,16 +751,20 @@ func (t *Transport) handleWebSocketUpgrade(w http.ResponseWriter, r *http.Reques go t.keepalive(pc) } -// performHandshake initiates handshake with a peer. func (t *Transport) performHandshake(pc *PeerConnection) error { // Set handshake timeout handshakeTimeout := 10 * time.Second - pc.Conn.SetWriteDeadline(time.Now().Add(handshakeTimeout)) - pc.Conn.SetReadDeadline(time.Now().Add(handshakeTimeout)) + connection := pc.webSocketConnection() + if connection == nil { + return core.E("Transport.performHandshake", "websocket connection is nil", nil) + } + + connection.SetWriteDeadline(time.Now().Add(handshakeTimeout)) + connection.SetReadDeadline(time.Now().Add(handshakeTimeout)) defer func() { // Reset deadlines after handshake - pc.Conn.SetWriteDeadline(time.Time{}) - pc.Conn.SetReadDeadline(time.Time{}) + connection.SetWriteDeadline(time.Time{}) + connection.SetReadDeadline(time.Time{}) }() identity := t.nodeManager.GetIdentity() @@ -778,12 +795,12 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return core.E("Transport.performHandshake", "marshal handshake message", err) } - if err := pc.Conn.WriteMessage(websocket.TextMessage, data); err != nil { + if err := connection.WriteMessage(websocket.TextMessage, data); err != nil { return core.E("Transport.performHandshake", "send handshake", err) } // Wait for ack - _, ackData, err := pc.Conn.ReadMessage() + _, ackData, err := connection.ReadMessage() if err != nil { return core.E("Transport.performHandshake", "read handshake ack", err) } @@ -844,7 +861,6 @@ func (t *Transport) performHandshake(pc *PeerConnection) error { return nil } -// readLoop reads messages from a peer connection. func (t *Transport) readLoop(pc *PeerConnection) { defer t.waitGroup.Done() defer t.removeConnection(pc) @@ -854,7 +870,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { if maxSize <= 0 { maxSize = DefaultMaxMessageSize } - pc.Conn.SetReadLimit(maxSize) + connection := pc.webSocketConnection() + if connection == nil { + return + } + + connection.SetReadLimit(maxSize) for { select { @@ -865,12 +886,12 @@ func (t *Transport) readLoop(pc *PeerConnection) { // Set read deadline to prevent blocking forever on unresponsive connections readDeadline := t.config.PingInterval + t.config.PongTimeout - if err := pc.Conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { + if err := connection.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { logging.Error("SetReadDeadline error", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return } - _, data, err := pc.Conn.ReadMessage() + _, data, err := connection.ReadMessage() if err != nil { logging.Debug("read error from peer", logging.Fields{"peer_id": pc.Peer.ID, "error": err}) return @@ -913,7 +934,6 @@ func (t *Transport) readLoop(pc *PeerConnection) { } } -// keepalive sends periodic pings. func (t *Transport) keepalive(pc *PeerConnection) { defer t.waitGroup.Done() @@ -948,7 +968,6 @@ func (t *Transport) keepalive(pc *PeerConnection) { } } -// removeConnection removes and cleans up a connection. func (t *Transport) removeConnection(pc *PeerConnection) { t.mutex.Lock() delete(t.connections, pc.Peer.ID) @@ -972,19 +991,28 @@ func (pc *PeerConnection) sendLocked(msg *Message) error { return err } - if err := pc.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + connection := pc.webSocketConnection() + if connection == nil { + return core.E("PeerConnection.Send", "websocket connection is nil", nil) + } + + if err := connection.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { return core.E("PeerConnection.Send", "failed to set write deadline", err) } - defer pc.Conn.SetWriteDeadline(time.Time{}) + defer connection.SetWriteDeadline(time.Time{}) - return pc.Conn.WriteMessage(websocket.BinaryMessage, data) + return connection.WriteMessage(websocket.BinaryMessage, data) } // err := peerConnection.Close() func (pc *PeerConnection) Close() error { var err error pc.closeOnce.Do(func() { - err = pc.Conn.Close() + connection := pc.webSocketConnection() + if connection == nil { + return + } + err = connection.Close() }) return err } @@ -1011,6 +1039,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { pc.writeMutex.Lock() defer pc.writeMutex.Unlock() + connection := pc.webSocketConnection() + if connection == nil { + return + } + // Try to send disconnect message (best effort). if pc.transport != nil && pc.SharedSecret != nil { identity := pc.transport.nodeManager.GetIdentity() @@ -1027,12 +1060,11 @@ func (pc *PeerConnection) GracefulClose(reason string, code int) error { } // Close the underlying connection - err = pc.Conn.Close() + err = connection.Close() }) return err } -// encryptMessage encrypts a message using SMSG with the shared secret. func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, error) { // Serialize message to JSON (using pooled buffer for efficiency) msgData, err := MarshalJSON(msg) @@ -1053,7 +1085,6 @@ func (t *Transport) encryptMessage(msg *Message, sharedSecret []byte) ([]byte, e return encrypted, nil } -// decryptMessage decrypts a message using SMSG with the shared secret. func (t *Transport) decryptMessage(data []byte, sharedSecret []byte) (*Message, error) { // Decrypt using shared secret as password password := base64.StdEncoding.EncodeToString(sharedSecret) diff --git a/node/transport_test.go b/node/transport_test.go index 16acd46..5d00ab7 100644 --- a/node/transport_test.go +++ b/node/transport_test.go @@ -562,7 +562,7 @@ func TestTransport_KeepaliveTimeout_Bad(t *testing.T) { if clientConn == nil { t.Fatal("client should have connection to server") } - clientConn.Conn.Close() + clientConn.WebSocketConnection.Close() // Wait for server to detect and clean up deadline := time.After(2 * time.Second) @@ -779,7 +779,7 @@ func TestTransport_StartAndStop_Good(t *testing.T) { nm := newTestNodeManager(t, "start-test", RoleWorker) reg := newTestPeerRegistry(t) cfg := DefaultTransportConfig() - cfg.ListenAddr = ":0" // Let OS pick a free port + cfg.ListenAddress = ":0" // Let OS pick a free port tr := NewTransport(nm, reg, cfg) diff --git a/specs/node.md b/specs/node.md index 99f6eb8..d00f942 100644 --- a/specs/node.md +++ b/specs/node.md @@ -25,14 +25,14 @@ | `NodeRole` | `type NodeRole string` | Operational mode string for controller, worker, or dual-role nodes. | | `Peer` | `struct{ ID string; Name string; PublicKey string; Address string; Role NodeRole; AddedAt time.Time; LastSeen time.Time; PingMS float64; Hops int; GeoKM float64; Score float64; Connected bool }` | Registry record for a remote node, including addressing, role, scoring metrics, and transient connection state. | | `PeerAuthMode` | `type PeerAuthMode int` | Peer admission policy used by `PeerRegistry` when unknown peers attempt to connect. | -| `PeerConnection` | `struct{ Peer *Peer; Conn *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | +| `PeerConnection` | `struct{ Peer *Peer; WebSocketConnection *websocket.Conn; SharedSecret []byte; LastActivity time.Time }` | Active WebSocket session to a peer, including the negotiated shared secret and transport-owned write/close coordination. | | `PeerRateLimiter` | `struct{ /* unexported fields */ }` | Per-peer token bucket limiter used by the transport hot path. | | `PeerRegistry` | `struct{ /* unexported fields */ }` | Concurrent peer store with KD-tree selection, allowlist state, and debounced persistence to disk. | | `ProtocolError` | `struct{ Code int; Message string }` | Structured remote error returned by protocol response helpers when a peer replies with `MsgError`. | | `RawMessage` | `type RawMessage []byte` | Raw JSON payload bytes preserved without eager decoding. | | `ResponseHandler` | `struct{}` | Helper for validating message envelopes and decoding typed responses. | | `Transport` | `struct{ /* unexported fields */ }` | WebSocket transport that manages listeners, connections, encryption, deduplication, and shutdown coordination. | -| `TransportConfig` | `struct{ ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | +| `TransportConfig` | `struct{ ListenAddress string; ListenAddr string; WebSocketPath string; TLSCertPath string; TLSKeyPath string; MaxConnections int; MaxMessageSize int64; PingInterval time.Duration; PongTimeout time.Duration }` | Listener, TLS, sizing, and keepalive settings for `Transport`. | | `Worker` | `struct{ DataDir string /* plus unexported fields */ }` | Inbound command handler for worker nodes. It tracks uptime, optional miner/profile integrations, and the base directory used for deployments. | ### Payload and integration types @@ -88,7 +88,7 @@ | Name | Signature | Description | | --- | --- | --- | -| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | +| `DefaultTransportConfig` | `func DefaultTransportConfig() TransportConfig` | Returns the transport defaults: `ListenAddress=:9091`, `ListenAddr=:9091`, `WebSocketPath=/ws`, `MaxConnections=100`, `MaxMessageSize=1<<20`, `PingInterval=30s`, and `PongTimeout=10s`. | | `NewController` | `func NewController(node *NodeManager, peers *PeerRegistry, transport *Transport) *Controller` | Creates a controller, initialises its pending-response map, and installs its response handler on `transport`. | | `NewDispatcher` | `func NewDispatcher() *Dispatcher` | Creates an empty dispatcher with a debug-level component logger named `dispatcher`. | | `NewMessageDeduplicator` | `func NewMessageDeduplicator(ttl time.Duration) *MessageDeduplicator` | Creates a deduplicator that retains message IDs for the supplied TTL. |