From abc88f5c7abc5b836998ca0d5df25d9864cb8a98 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 19:23:28 +0000 Subject: [PATCH] feat(levin): portable storage varint encode/decode Co-Authored-By: Charon --- node/levin/varint.go | 93 +++++++++++++++++++++++++ node/levin/varint_test.go | 140 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 node/levin/varint.go create mode 100644 node/levin/varint_test.go diff --git a/node/levin/varint.go b/node/levin/varint.go new file mode 100644 index 0000000..cb40d52 --- /dev/null +++ b/node/levin/varint.go @@ -0,0 +1,93 @@ +// Copyright (c) 2024-2026 Lethean Contributors +// SPDX-License-Identifier: EUPL-1.2 + +package levin + +import ( + "encoding/binary" + "errors" +) + +// Size-mark bits occupying the two lowest bits of the first byte. +const ( + varintMask = 0x03 + varintMark1 = 0x00 // 1 byte, max 63 + varintMark2 = 0x01 // 2 bytes, max 16,383 + varintMark4 = 0x02 // 4 bytes, max 1,073,741,823 + varintMark8 = 0x03 // 8 bytes, max 4,611,686,018,427,387,903 + varintMax1 = 63 + varintMax2 = 16_383 + varintMax4 = 1_073_741_823 + varintMax8 = 4_611_686_018_427_387_903 +) + +// ErrVarintTruncated is returned when the buffer is too short. +var ErrVarintTruncated = errors.New("levin: truncated varint") + +// ErrVarintOverflow is returned when the value is too large to encode. +var ErrVarintOverflow = errors.New("levin: varint overflow") + +// PackVarint encodes v 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. +func PackVarint(v 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 + default: + raw := (v << 2) | varintMark8 + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, raw) + return buf + } +} + +// UnpackVarint decodes one epee portable-storage varint from buf. +// It returns the decoded value, the number of bytes consumed, and any error. +func UnpackVarint(buf []byte) (value uint64, bytesConsumed int, err error) { + if len(buf) == 0 { + return 0, 0, ErrVarintTruncated + } + + mark := buf[0] & varintMask + + switch mark { + case varintMark1: + value = uint64(buf[0]) >> 2 + return value, 1, nil + case varintMark2: + if len(buf) < 2 { + return 0, 0, ErrVarintTruncated + } + raw := binary.LittleEndian.Uint16(buf[:2]) + value = uint64(raw) >> 2 + return value, 2, nil + case varintMark4: + if len(buf) < 4 { + return 0, 0, ErrVarintTruncated + } + raw := binary.LittleEndian.Uint32(buf[:4]) + value = uint64(raw) >> 2 + return value, 4, nil + case varintMark8: + if len(buf) < 8 { + return 0, 0, ErrVarintTruncated + } + raw := binary.LittleEndian.Uint64(buf[:8]) + value = raw >> 2 + return value, 8, nil + default: + // Unreachable — mark is masked to 2 bits. + return 0, 0, ErrVarintTruncated + } +} diff --git a/node/levin/varint_test.go b/node/levin/varint_test.go new file mode 100644 index 0000000..2082864 --- /dev/null +++ b/node/levin/varint_test.go @@ -0,0 +1,140 @@ +// 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 TestPackVarint_Value5(t *testing.T) { + // 5 << 2 | 0x00 = 20 = 0x14 + got := PackVarint(5) + assert.Equal(t, []byte{0x14}, got) +} + +func TestPackVarint_Value100(t *testing.T) { + // 100 << 2 | 0x01 = 401 = 0x0191 → LE [0x91, 0x01] + got := PackVarint(100) + assert.Equal(t, []byte{0x91, 0x01}, got) +} + +func TestPackVarint_Value65536(t *testing.T) { + // 65536 << 2 | 0x02 = 262146 = 0x00040002 → LE [0x02, 0x00, 0x04, 0x00] + got := PackVarint(65536) + assert.Equal(t, []byte{0x02, 0x00, 0x04, 0x00}, got) +} + +func TestPackVarint_Value2Billion(t *testing.T) { + got := PackVarint(2_000_000_000) + require.Len(t, got, 8) + // Low 2 bits must be 0x03 (8-byte mark). + assert.Equal(t, byte(0x03), got[0]&0x03) +} + +func TestPackVarint_Zero(t *testing.T) { + got := PackVarint(0) + assert.Equal(t, []byte{0x00}, got) +} + +func TestPackVarint_Boundaries(t *testing.T) { + tests := []struct { + name string + value uint64 + wantLen int + }{ + {"1-byte max (63)", 63, 1}, + {"2-byte min (64)", 64, 2}, + {"2-byte max (16383)", 16_383, 2}, + {"4-byte min (16384)", 16_384, 4}, + {"4-byte max (1073741823)", 1_073_741_823, 4}, + {"8-byte min (1073741824)", 1_073_741_824, 8}, + {"8-byte max", 4_611_686_018_427_387_903, 8}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := PackVarint(tc.value) + assert.Len(t, got, tc.wantLen, "wrong length for value %d", tc.value) + }) + } +} + +func TestVarint_RoundTrip(t *testing.T) { + values := []uint64{ + 0, 1, 63, 64, 100, 16_383, 16_384, + 1_073_741_823, 1_073_741_824, + 4_611_686_018_427_387_903, + } + + for _, v := range values { + buf := PackVarint(v) + decoded, consumed, err := UnpackVarint(buf) + require.NoError(t, err, "value %d", v) + assert.Equal(t, v, decoded, "mismatch for value %d", v) + assert.Equal(t, len(buf), consumed, "wrong bytes consumed for value %d", v) + } +} + +func TestUnpackVarint_EmptyInput(t *testing.T) { + _, _, err := UnpackVarint([]byte{}) + require.Error(t, err) + assert.ErrorIs(t, err, ErrVarintTruncated) +} + +func TestUnpackVarint_Truncated2Byte(t *testing.T) { + // Encode 64 (needs 2 bytes), then only pass 1 byte. + buf := PackVarint(64) + require.Len(t, buf, 2) + _, _, err := UnpackVarint(buf[:1]) + require.Error(t, err) + assert.ErrorIs(t, err, ErrVarintTruncated) +} + +func TestUnpackVarint_Truncated4Byte(t *testing.T) { + buf := PackVarint(16_384) + require.Len(t, buf, 4) + _, _, err := UnpackVarint(buf[:2]) + require.Error(t, err) + assert.ErrorIs(t, err, ErrVarintTruncated) +} + +func TestUnpackVarint_Truncated8Byte(t *testing.T) { + buf := PackVarint(1_073_741_824) + require.Len(t, buf, 8) + _, _, err := UnpackVarint(buf[:4]) + require.Error(t, err) + assert.ErrorIs(t, err, ErrVarintTruncated) +} + +func TestUnpackVarint_ExtraBytes(t *testing.T) { + // Ensure that extra trailing bytes are not consumed. + buf := append(PackVarint(42), 0xFF, 0xFF) + decoded, consumed, err := UnpackVarint(buf) + require.NoError(t, err) + assert.Equal(t, uint64(42), decoded) + assert.Equal(t, 1, consumed) +} + +func TestPackVarint_SizeMarkBits(t *testing.T) { + tests := []struct { + name string + value uint64 + wantMark byte + }{ + {"1-byte", 0, 0x00}, + {"2-byte", 64, 0x01}, + {"4-byte", 16_384, 0x02}, + {"8-byte", 1_073_741_824, 0x03}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := PackVarint(tc.value) + assert.Equal(t, tc.wantMark, got[0]&0x03) + }) + } +}