feat(levin): header encode/decode (33-byte Levin packet framing)
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
712fe326ff
commit
7089e0990c
2 changed files with 263 additions and 0 deletions
95
node/levin/header.go
Normal file
95
node/levin/header.go
Normal file
|
|
@ -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
|
||||
}
|
||||
168
node/levin/header_test.go
Normal file
168
node/levin/header_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue