From 7089e0990c044f16ab75aced9976ff6dab315fa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 19:23:24 +0000 Subject: [PATCH] feat(levin): header encode/decode (33-byte Levin packet framing) Co-Authored-By: Charon --- node/levin/header.go | 95 +++++++++++++++++++++ node/levin/header_test.go | 168 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 node/levin/header.go create mode 100644 node/levin/header_test.go diff --git a/node/levin/header.go b/node/levin/header.go new file mode 100644 index 0000000..4655b03 --- /dev/null +++ b/node/levin/header.go @@ -0,0 +1,95 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +// Package levin implements the CryptoNote Levin binary protocol. +// It is a standalone package with no imports from the parent node package. +package levin + +import ( + "encoding/binary" + "errors" +) + +// HeaderSize is the exact byte length of a serialised Levin header. +const HeaderSize = 33 + +// Signature is the magic value that opens every Levin packet. +const Signature uint64 = 0x0101010101012101 + +// MaxPayloadSize is the upper bound we accept for a single payload (100 MB). +const MaxPayloadSize uint64 = 100 * 1024 * 1024 + +// Return-code constants carried in every Levin response. +const ( + ReturnOK int32 = 0 + ReturnErrConnection int32 = -1 + ReturnErrFormat int32 = -7 + ReturnErrSignature int32 = -13 +) + +// Command IDs for the CryptoNote P2P layer. +const ( + CommandHandshake uint32 = 1001 + CommandTimedSync uint32 = 1002 + CommandPing uint32 = 1003 + CommandNewBlock uint32 = 2001 + CommandNewTransactions uint32 = 2002 + CommandRequestObjects uint32 = 2003 + CommandResponseObjects uint32 = 2004 + CommandRequestChain uint32 = 2006 + CommandResponseChain uint32 = 2007 +) + +// Sentinel errors returned by DecodeHeader. +var ( + ErrBadSignature = errors.New("levin: bad signature") + ErrPayloadTooBig = errors.New("levin: payload exceeds maximum size") +) + +// Header is the 33-byte packed header that prefixes every Levin message. +type Header struct { + Signature uint64 + PayloadSize uint64 + ExpectResponse bool + Command uint32 + ReturnCode int32 + Flags uint32 + ProtocolVersion uint32 +} + +// EncodeHeader serialises h into a fixed-size 33-byte array (little-endian). +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 + } else { + buf[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 +} + +// DecodeHeader deserialises a 33-byte array into a Header, validating +// the magic signature. +func DecodeHeader(buf [HeaderSize]byte) (Header, error) { + var h Header + h.Signature = binary.LittleEndian.Uint64(buf[0:8]) + if h.Signature != Signature { + return Header{}, ErrBadSignature + } + h.PayloadSize = binary.LittleEndian.Uint64(buf[8:16]) + if h.PayloadSize > MaxPayloadSize { + return Header{}, ErrPayloadTooBig + } + 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 +} diff --git a/node/levin/header_test.go b/node/levin/header_test.go new file mode 100644 index 0000000..4edfdaf --- /dev/null +++ b/node/levin/header_test.go @@ -0,0 +1,168 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +package levin + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHeaderSizeIs33(t *testing.T) { + assert.Equal(t, 33, HeaderSize) +} + +func TestEncodeHeader_KnownValues(t *testing.T) { + h := &Header{ + Signature: Signature, + PayloadSize: 256, + ExpectResponse: true, + Command: CommandHandshake, + ReturnCode: ReturnOK, + Flags: 0, + ProtocolVersion: 0, + } + + buf := EncodeHeader(h) + + // Verify signature at offset 0. + sig := binary.LittleEndian.Uint64(buf[0:8]) + assert.Equal(t, Signature, sig) + + // Verify payload size at offset 8. + ps := binary.LittleEndian.Uint64(buf[8:16]) + assert.Equal(t, uint64(256), ps) + + // Verify expect-response at offset 16. + assert.Equal(t, byte(0x01), buf[16]) + + // Verify command at offset 17. + cmd := binary.LittleEndian.Uint32(buf[17:21]) + assert.Equal(t, CommandHandshake, cmd) + + // Verify return code at offset 21. + rc := int32(binary.LittleEndian.Uint32(buf[21:25])) + assert.Equal(t, ReturnOK, rc) + + // Verify flags at offset 25. + flags := binary.LittleEndian.Uint32(buf[25:29]) + assert.Equal(t, uint32(0), flags) + + // Verify protocol version at offset 29. + pv := binary.LittleEndian.Uint32(buf[29:33]) + assert.Equal(t, uint32(0), pv) +} + +func TestEncodeHeader_ExpectResponseFalse(t *testing.T) { + h := &Header{ + Signature: Signature, + PayloadSize: 42, + ExpectResponse: false, + Command: CommandPing, + ReturnCode: ReturnOK, + } + buf := EncodeHeader(h) + assert.Equal(t, byte(0x00), buf[16]) +} + +func TestEncodeHeader_NegativeReturnCode(t *testing.T) { + h := &Header{ + Signature: Signature, + PayloadSize: 0, + ExpectResponse: false, + Command: CommandHandshake, + ReturnCode: ReturnErrFormat, + } + buf := EncodeHeader(h) + rc := int32(binary.LittleEndian.Uint32(buf[21:25])) + assert.Equal(t, ReturnErrFormat, rc) +} + +func TestDecodeHeader_RoundTrip(t *testing.T) { + original := &Header{ + Signature: Signature, + PayloadSize: 1024, + ExpectResponse: true, + Command: CommandTimedSync, + ReturnCode: ReturnErrConnection, + Flags: 0, + ProtocolVersion: 0, + } + + buf := EncodeHeader(original) + decoded, err := DecodeHeader(buf) + require.NoError(t, err) + + assert.Equal(t, original.Signature, decoded.Signature) + assert.Equal(t, original.PayloadSize, decoded.PayloadSize) + assert.Equal(t, original.ExpectResponse, decoded.ExpectResponse) + assert.Equal(t, original.Command, decoded.Command) + assert.Equal(t, original.ReturnCode, decoded.ReturnCode) + assert.Equal(t, original.Flags, decoded.Flags) + assert.Equal(t, original.ProtocolVersion, decoded.ProtocolVersion) +} + +func TestDecodeHeader_AllCommands(t *testing.T) { + commands := []uint32{ + CommandHandshake, + CommandTimedSync, + CommandPing, + CommandNewBlock, + CommandNewTransactions, + CommandRequestObjects, + CommandResponseObjects, + CommandRequestChain, + CommandResponseChain, + } + + for _, cmd := range commands { + h := &Header{ + Signature: Signature, + Command: cmd, + ReturnCode: ReturnOK, + } + buf := EncodeHeader(h) + decoded, err := DecodeHeader(buf) + require.NoError(t, err) + assert.Equal(t, cmd, decoded.Command) + } +} + +func TestDecodeHeader_BadSignature(t *testing.T) { + h := &Header{ + Signature: 0xDEADBEEF, + PayloadSize: 0, + Command: CommandPing, + } + buf := EncodeHeader(h) + _, err := DecodeHeader(buf) + require.Error(t, err) + assert.ErrorIs(t, err, ErrBadSignature) +} + +func TestDecodeHeader_PayloadTooBig(t *testing.T) { + h := &Header{ + Signature: Signature, + PayloadSize: MaxPayloadSize + 1, + Command: CommandHandshake, + } + buf := EncodeHeader(h) + _, err := DecodeHeader(buf) + require.Error(t, err) + assert.ErrorIs(t, err, ErrPayloadTooBig) +} + +func TestDecodeHeader_MaxPayloadExact(t *testing.T) { + h := &Header{ + Signature: Signature, + PayloadSize: MaxPayloadSize, + Command: CommandHandshake, + } + buf := EncodeHeader(h) + decoded, err := DecodeHeader(buf) + require.NoError(t, err) + assert.Equal(t, MaxPayloadSize, decoded.PayloadSize) +}