From a2b7f1bfabd75d847a6ee80b7fc94cb12fc8ada5 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 20:42:25 +0700 Subject: [PATCH 1/5] Update implementation plan --- docs/IMPLEMENTATION.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index fda7242..ff6da45 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -60,22 +60,27 @@ - [X] Add workflow tests with real PoW implementations - [X] Add unsuccessful flow tests for invalid solutions -## Phase 4: Basic Server Architecture +## Phase 4: Binary Protocol Implementation +- [ ] Implement binary message protocol codec with Reader/Writer abstraction +- [ ] Create protocol message types and structures +- [ ] Add message serialization/deserialization (JSON) +- [ ] Implement protocol parsing with proper error handling +- [ ] Create message validation and bounds checking +- [ ] Write unit tests for protocol components + +## Phase 5: Basic Server Architecture - [ ] Set up structured logging (zerolog/logrus) - [ ] Set up metrics collection (prometheus) - [ ] Create configuration management - [ ] Integrate all components into server architecture -## Phase 5: TCP Protocol Implementation -- [ ] Implement binary message protocol codec -- [ ] Create protocol message types and structures +## Phase 6: TCP Protocol & Connection Handling - [ ] Implement connection handler with proper error handling -- [ ] Add message serialization/deserialization (JSON) - [ ] Create protocol state machine - [ ] Implement connection lifecycle management -- [ ] Write unit tests for protocol components +- [ ] Add connection timeout and lifecycle management -## Phase 6: Server Core & Request Handling +## Phase 7: Server Core & Request Handling - [ ] Implement TCP server with connection pooling - [ ] Create request router and handler dispatcher - [ ] Add connection timeout and lifecycle management @@ -84,7 +89,7 @@ - [ ] Create health check endpoints - [ ] Write integration tests for server core -## Phase 7: DDOS Protection & Rate Limiting +## Phase 9: DDOS Protection & Rate Limiting - [ ] Implement IP-based connection limiting - [ ] Create rate limiting service with time windows - [ ] Add automatic difficulty adjustment based on load @@ -93,7 +98,7 @@ - [ ] Add monitoring for attack detection - [ ] Write tests for protection mechanisms -## Phase 8: Observability & Monitoring +## Phase 9: Observability & Monitoring - [ ] Add structured logging throughout application - [ ] Implement metrics for key performance indicators: - [ ] Active connections count @@ -105,7 +110,7 @@ - [ ] Add error categorization and reporting - [ ] Implement health check endpoints -## Phase 9: Configuration & Environment Setup +## Phase 10: Configuration & Environment Setup - [ ] Create configuration structure with validation - [ ] Support environment variables and config files - [ ] Add configuration for different environments (dev/prod) @@ -113,7 +118,7 @@ - [ ] Create deployment configuration templates - [ ] Add configuration validation and defaults -## Phase 10: Client Implementation +## Phase 11: Client Implementation - [ ] Create client application structure - [ ] Implement PoW solver algorithm - [ ] Create client-side protocol implementation @@ -123,7 +128,7 @@ - [ ] Add client metrics and logging - [ ] Write client unit and integration tests -## Phase 11: Docker & Deployment +## Phase 12: Docker & Deployment - [ ] Create multi-stage Dockerfile for server - [ ] Create Dockerfile for client - [ ] Create docker-compose.yml for local development @@ -132,7 +137,7 @@ - [ ] Add environment-specific configurations - [ ] Create deployment documentation -## Phase 12: Testing & Quality Assurance +## Phase 13: Testing & Quality Assurance - [ ] Write comprehensive unit tests (>80% coverage): - [ ] PoW algorithm tests - [ ] Protocol handler tests @@ -147,7 +152,7 @@ - [ ] Add benchmark tests for performance validation - [ ] Create stress testing scenarios -## Phase 13: Documentation & Final Polish +## Phase 14: Documentation & Final Polish - [ ] Write comprehensive README with setup instructions - [ ] Create API documentation for all interfaces - [ ] Add inline code documentation @@ -156,7 +161,7 @@ - [ ] Add performance tuning recommendations - [ ] Create monitoring and alerting guide -## Phase 14: Production Readiness Checklist +## Phase 15: Production Readiness Checklist - [ ] Security audit of all components - [ ] Performance benchmarking and optimization - [ ] Memory leak detection and prevention -- 2.44.1 From ffc23c362bdda5fec5b12fe7bb630c77af78bc42 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 21:03:42 +0700 Subject: [PATCH 2/5] Define presentation layer types --- internal/protocol/types.go | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 internal/protocol/types.go diff --git a/internal/protocol/types.go b/internal/protocol/types.go new file mode 100644 index 0000000..4afd17f --- /dev/null +++ b/internal/protocol/types.go @@ -0,0 +1,64 @@ +package protocol + +import ( + "hash-of-wisdom/internal/pow/challenge" + "hash-of-wisdom/internal/quotes" +) + +// MessageType represents the type of protocol message +type MessageType byte + +const ( + ChallengeRequest MessageType = 0x01 + ChallengeResponse MessageType = 0x02 + SolutionRequest MessageType = 0x03 + QuoteResponse MessageType = 0x04 + ErrorResponse MessageType = 0x05 +) + +// Message represents a protocol message with type and payload +type Message struct { + Type MessageType + Payload []byte +} + +// ChallengeRequestPayload is empty (no payload for challenge requests) +type ChallengeRequestPayload struct{} + +// ChallengeResponsePayload is the direct challenge object (not wrapped) +type ChallengeResponsePayload challenge.Challenge + +// SolutionRequestPayload contains the client's solution attempt +type SolutionRequestPayload struct { + Challenge challenge.Challenge `json:"challenge"` + Nonce uint64 `json:"nonce"` +} + +// QuoteResponsePayload is the direct quote object (not wrapped) +type QuoteResponsePayload quotes.Quote + +// ErrorResponsePayload contains error information +type ErrorResponsePayload struct { + Code string `json:"code"` + Message string `json:"message"` + RetryAfter int `json:"retry_after,omitempty"` + Details map[string]string `json:"details,omitempty"` +} + +// Error codes as defined in protocol specification +const ( + ErrMalformedMessage = "MALFORMED_MESSAGE" + ErrInvalidChallenge = "INVALID_CHALLENGE" + ErrInvalidSolution = "INVALID_SOLUTION" + ErrExpiredChallenge = "EXPIRED_CHALLENGE" + ErrRateLimited = "RATE_LIMITED" + ErrServerError = "SERVER_ERROR" + ErrTooManyConnections = "TOO_MANY_CONNECTIONS" + ErrDifficultyTooHigh = "DIFFICULTY_TOO_HIGH" +) + +// Protocol constants +const ( + MaxPayloadSize = 8 * 1024 // 8KB maximum payload size + HeaderSize = 5 // 1 byte type + 4 bytes length +) -- 2.44.1 From dc9f2b24d64b4865eba9ac01d96f61cd0703d639 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 21:03:54 +0700 Subject: [PATCH 3/5] Implement codec --- internal/protocol/codec.go | 111 +++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 internal/protocol/codec.go diff --git a/internal/protocol/codec.go b/internal/protocol/codec.go new file mode 100644 index 0000000..e5d83cd --- /dev/null +++ b/internal/protocol/codec.go @@ -0,0 +1,111 @@ +package protocol + +import ( + "encoding/binary" + "fmt" + "io" + "unicode/utf8" +) + +// Codec handles encoding and decoding of protocol messages +type Codec struct{} + +// NewCodec creates a new protocol codec +func NewCodec() *Codec { + return &Codec{} +} + +// Encode writes a message to the writer using the protocol format +func (c *Codec) Encode(w io.Writer, msg *Message) error { + if msg == nil { + return fmt.Errorf("message cannot be nil") + } + + // Validate payload size + if len(msg.Payload) > MaxPayloadSize { + return fmt.Errorf("payload size %d exceeds maximum %d", len(msg.Payload), MaxPayloadSize) + } + + // Write message type (1 byte) + if err := binary.Write(w, binary.BigEndian, msg.Type); err != nil { + return fmt.Errorf("failed to write message type: %w", err) + } + + // Write payload length (4 bytes, big-endian) + payloadLength := uint32(len(msg.Payload)) + if err := binary.Write(w, binary.BigEndian, payloadLength); err != nil { + return fmt.Errorf("failed to write payload length: %w", err) + } + + // Write payload if present + if len(msg.Payload) > 0 { + if _, err := w.Write(msg.Payload); err != nil { + return fmt.Errorf("failed to write payload: %w", err) + } + } + + return nil +} + +// Decode reads a message from the reader using the protocol format +func (c *Codec) Decode(r io.Reader) (*Message, error) { + // Read message type (1 byte) + var msgType MessageType + if err := binary.Read(r, binary.BigEndian, &msgType); err != nil { + if err == io.EOF { + return nil, err + } + return nil, fmt.Errorf("failed to read message type: %w", err) + } + + // Validate message type + if !isValidMessageType(msgType) { + return nil, fmt.Errorf("invalid message type: 0x%02x", msgType) + } + + // Read payload length (4 bytes, big-endian) + var payloadLength uint32 + if err := binary.Read(r, binary.BigEndian, &payloadLength); err != nil { + return nil, fmt.Errorf("failed to read payload length: %w", err) + } + + // Validate payload length + if payloadLength > MaxPayloadSize { + return nil, fmt.Errorf("payload length %d exceeds maximum %d", payloadLength, MaxPayloadSize) + } + + // Read payload if present + var payload []byte + if payloadLength > 0 { + payload = make([]byte, payloadLength) + // Use LimitReader to ensure we don't read more than payloadLength bytes + // even if the underlying reader has more data available + limitedReader := io.LimitReader(r, int64(payloadLength)) + // Note: ReadFull may block waiting for data. The connection handler + // MUST set appropriate read deadlines to prevent slowloris attacks + if _, err := io.ReadFull(limitedReader, payload); err != nil { + return nil, fmt.Errorf("failed to read payload: %w", err) + } + + // Validate payload is valid UTF-8 + if !utf8.Valid(payload) { + return nil, fmt.Errorf("payload contains invalid UTF-8") + } + } + + return &Message{ + Type: msgType, + Payload: payload, + }, nil +} + + +// isValidMessageType checks if the message type is defined in the protocol +func isValidMessageType(msgType MessageType) bool { + switch msgType { + case ChallengeRequest, ChallengeResponse, SolutionRequest, QuoteResponse, ErrorResponse: + return true + default: + return false + } +} -- 2.44.1 From 7db1a401d3b88653198d06d7832ab3b9b36ad866 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 21:04:06 +0700 Subject: [PATCH 4/5] Implement codec tests --- internal/protocol/codec_test.go | 308 ++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 internal/protocol/codec_test.go diff --git a/internal/protocol/codec_test.go b/internal/protocol/codec_test.go new file mode 100644 index 0000000..6e5d6d5 --- /dev/null +++ b/internal/protocol/codec_test.go @@ -0,0 +1,308 @@ +package protocol + +import ( + "bytes" + "encoding/json" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCodec_Encode_Decode(t *testing.T) { + codec := NewCodec() + + tests := []struct { + name string + message *Message + }{ + { + name: "challenge request (empty payload)", + message: &Message{ + Type: ChallengeRequest, + Payload: []byte{}, + }, + }, + { + name: "challenge response with payload", + message: &Message{ + Type: ChallengeResponse, + Payload: []byte(`{"challenge":{"timestamp":1640995200,"difficulty":4}}`), + }, + }, + { + name: "error response", + message: &Message{ + Type: ErrorResponse, + Payload: []byte(`{"code":"INVALID_SOLUTION","message":"Invalid nonce"}`), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + + // Encode message + err := codec.Encode(&buf, tt.message) + require.NoError(t, err) + + // Decode message + decoded, err := codec.Decode(&buf) + require.NoError(t, err) + + assert.Equal(t, tt.message.Type, decoded.Type) + if len(tt.message.Payload) == 0 && len(decoded.Payload) == 0 { + // Both are empty (nil or empty slice) + assert.True(t, true) + } else { + assert.Equal(t, tt.message.Payload, decoded.Payload) + } + }) + } +} + +func TestCodec_Encode_Errors(t *testing.T) { + codec := NewCodec() + + tests := []struct { + name string + message *Message + wantErr string + }{ + { + name: "nil message", + message: nil, + wantErr: "message cannot be nil", + }, + { + name: "payload too large", + message: &Message{ + Type: ChallengeRequest, + Payload: make([]byte, MaxPayloadSize+1), + }, + wantErr: "payload size", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + err := codec.Encode(&buf, tt.message) + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} + +func TestCodec_Decode_Errors(t *testing.T) { + codec := NewCodec() + + tests := []struct { + name string + data []byte + wantErr string + }{ + { + name: "empty data", + data: []byte{}, + wantErr: "EOF", + }, + { + name: "invalid message type", + data: []byte{0xFF, 0x00, 0x00, 0x00, 0x00}, + wantErr: "invalid message type", + }, + { + name: "incomplete header", + data: []byte{0x01, 0x00, 0x00}, + wantErr: "failed to read payload length", + }, + { + name: "payload too large", + data: append([]byte{0x01}, encodeBigEndianUint32(MaxPayloadSize+1)...), + wantErr: "payload length", + }, + { + name: "incomplete payload", + data: []byte{0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02}, + wantErr: "failed to read payload", + }, + { + name: "invalid UTF-8 in payload", + data: []byte{0x01, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFE, 0xFD}, + wantErr: "invalid UTF-8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer(tt.data) + _, err := codec.Decode(buf) + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} + + +func TestCodec_RoundTrip_RealPayloads(t *testing.T) { + codec := NewCodec() + + t.Run("challenge response round trip", func(t *testing.T) { + original := &ChallengeResponsePayload{ + Timestamp: time.Now().Unix(), + Difficulty: 4, + Resource: "quotes", + Random: []byte("random123"), + HMAC: []byte("hmac_signature"), + } + + // Marshal payload + jsonData, err := json.Marshal(original) + require.NoError(t, err) + + msg := &Message{ + Type: ChallengeResponse, + Payload: jsonData, + } + + // Simulate network transmission + var buf bytes.Buffer + err = codec.Encode(&buf, msg) + require.NoError(t, err) + + // Decode message + decoded, err := codec.Decode(&buf) + require.NoError(t, err) + + // Unmarshal payload + var result ChallengeResponsePayload + err = json.Unmarshal(decoded.Payload, &result) + require.NoError(t, err) + + assert.Equal(t, original.Timestamp, result.Timestamp) + assert.Equal(t, original.Difficulty, result.Difficulty) + assert.Equal(t, original.Resource, result.Resource) + assert.Equal(t, original.Random, result.Random) + assert.Equal(t, original.HMAC, result.HMAC) + }) + + t.Run("quote response round trip", func(t *testing.T) { + original := &QuoteResponsePayload{ + Text: "Test quote", + Author: "Test author", + } + + // Marshal payload + jsonData, err := json.Marshal(original) + require.NoError(t, err) + + msg := &Message{ + Type: QuoteResponse, + Payload: jsonData, + } + + var buf bytes.Buffer + err = codec.Encode(&buf, msg) + require.NoError(t, err) + + decoded, err := codec.Decode(&buf) + require.NoError(t, err) + + var result QuoteResponsePayload + err = json.Unmarshal(decoded.Payload, &result) + require.NoError(t, err) + + assert.Equal(t, original.Text, result.Text) + assert.Equal(t, original.Author, result.Author) + }) +} + +func TestCodec_WriteError_Handling(t *testing.T) { + codec := NewCodec() + + // Create a writer that fails after a certain number of bytes + failAfter := 3 + writer := &failingWriter{failAfter: failAfter} + + msg := &Message{ + Type: ChallengeResponse, + Payload: []byte("test payload"), + } + + err := codec.Encode(writer, msg) + assert.Error(t, err) +} + +func TestCodec_ReadError_Handling(t *testing.T) { + codec := NewCodec() + + // Create a reader that fails after reading header + reader := &failingReader{ + data: []byte{0x01, 0x00, 0x00, 0x00, 0x05}, + failAfter: 5, + } + + _, err := codec.Decode(reader) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read payload") +} + +// Helper functions and types for testing + +func encodeBigEndianUint32(val uint32) []byte { + return []byte{ + byte(val >> 24), + byte(val >> 16), + byte(val >> 8), + byte(val), + } +} + +type failingWriter struct { + written int + failAfter int +} + +func (w *failingWriter) Write(data []byte) (int, error) { + if w.written >= w.failAfter { + return 0, io.ErrShortWrite + } + + remaining := w.failAfter - w.written + if len(data) <= remaining { + w.written += len(data) + return len(data), nil + } + + w.written = w.failAfter + return remaining, io.ErrShortWrite +} + +type failingReader struct { + data []byte + pos int + failAfter int +} + +func (r *failingReader) Read(buf []byte) (int, error) { + if r.pos >= r.failAfter { + return 0, io.ErrUnexpectedEOF + } + + if r.pos >= len(r.data) { + return 0, io.EOF + } + + n := copy(buf, r.data[r.pos:]) + r.pos += n + + if r.pos >= r.failAfter { + return n, io.ErrUnexpectedEOF + } + + return n, nil +} -- 2.44.1 From 8a0bc48ac6aba60a821f80d00d058a0bb68ae9f5 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Fri, 22 Aug 2025 21:04:16 +0700 Subject: [PATCH 5/5] Update checkboxes --- docs/IMPLEMENTATION.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index ff6da45..8673278 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -61,12 +61,12 @@ - [X] Add unsuccessful flow tests for invalid solutions ## Phase 4: Binary Protocol Implementation -- [ ] Implement binary message protocol codec with Reader/Writer abstraction -- [ ] Create protocol message types and structures -- [ ] Add message serialization/deserialization (JSON) -- [ ] Implement protocol parsing with proper error handling -- [ ] Create message validation and bounds checking -- [ ] Write unit tests for protocol components +- [X] Implement binary message protocol codec with Reader/Writer abstraction +- [X] Create protocol message types and structures +- [X] Add message serialization/deserialization (JSON) +- [X] Implement protocol parsing with proper error handling +- [X] Create message validation and bounds checking +- [X] Write unit tests for protocol components ## Phase 5: Basic Server Architecture - [ ] Set up structured logging (zerolog/logrus) -- 2.44.1