This commit fixes a backwards compatibility issue that prevented nodes
from upgrading from v0.19.x to v0.20.x.
In v0.19.x, channel edge features were serialized as raw feature bytes
without a length prefix. In v0.20.x (commit 2f2845dfc), the serialization
changed to use Features.Encode() which adds a 2-byte big-endian length
prefix before the feature bits. The deserialization code was updated to
use Features.Decode() which expects this length prefix.
When v0.20.x reads a database created by v0.19.x, Decode() tries to read
a length prefix that doesn't exist, causing an EOF error:
unable to decode features: EOF
The fix adds a deserializeChanEdgeFeatures() helper that detects which
format is being read and decodes accordingly:
- New format (v0.20+): First 2 bytes encode the length of the remaining
bytes. Detected when uint16(bytes[0:2]) == len(bytes)-2.
- Legacy format (pre-v0.20): Raw feature bits without length prefix.
Uses DecodeBase256 with the known length.
The format detection is safe because in the legacy format, the first byte
always has at least one bit set (the serialization uses minimum bytes),
so the first two bytes can never encode a value equal to len-2.
Fixes #10528.
450 lines
14 KiB
Go
450 lines
14 KiB
Go
package graphdb
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"testing"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/stretchr/testify/require"
|
|
"pgregory.net/rapid"
|
|
)
|
|
|
|
// TestDeserializeChanEdgeFeaturesEmpty tests that empty feature bytes are
|
|
// handled correctly for both legacy and new formats.
|
|
func TestDeserializeChanEdgeFeaturesEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Empty bytes should result in empty features.
|
|
features, err := deserializeChanEdgeFeatures(nil)
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsEmpty())
|
|
|
|
features, err = deserializeChanEdgeFeatures([]byte{})
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsEmpty())
|
|
|
|
// New format with zero-length features: [0x00, 0x00].
|
|
features, err = deserializeChanEdgeFeatures([]byte{0x00, 0x00})
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsEmpty())
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesLegacyFormat tests deserialization of
|
|
// feature bytes written in the legacy format (pre-v0.20), which contains
|
|
// raw feature bits without a 2-byte length prefix.
|
|
func TestDeserializeChanEdgeFeaturesLegacyFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
legacyBytes []byte
|
|
expectedFeats []lnwire.FeatureBit
|
|
}{
|
|
{
|
|
name: "single byte - bit 0",
|
|
legacyBytes: []byte{0x01}, // bit 0 set
|
|
expectedFeats: []lnwire.FeatureBit{0},
|
|
},
|
|
{
|
|
name: "single byte - bit 7",
|
|
legacyBytes: []byte{0x80}, // bit 7 set
|
|
expectedFeats: []lnwire.FeatureBit{7},
|
|
},
|
|
{
|
|
name: "single byte - multiple bits",
|
|
legacyBytes: []byte{0x25}, // bits 0, 2, 5 set
|
|
expectedFeats: []lnwire.FeatureBit{0, 2, 5},
|
|
},
|
|
{
|
|
name: "two bytes - bit 8",
|
|
legacyBytes: []byte{0x01, 0x00}, // bit 8 set
|
|
expectedFeats: []lnwire.FeatureBit{8},
|
|
},
|
|
{
|
|
name: "two bytes - bits 0 and 15",
|
|
legacyBytes: []byte{0x80, 0x01}, // bits 0 and 15 set
|
|
expectedFeats: []lnwire.FeatureBit{0, 15},
|
|
},
|
|
{
|
|
// bit 1 (DataLossProtectOptional).
|
|
name: "common features - data loss protect",
|
|
legacyBytes: []byte{0x02},
|
|
expectedFeats: []lnwire.FeatureBit{
|
|
lnwire.DataLossProtectOptional,
|
|
},
|
|
},
|
|
{
|
|
// bits 1, 7, 9, 13, 15 = DataLossProtect,
|
|
// GossipQueries, TLVOnion, StaticRemoteKey,
|
|
// PaymentAddr.
|
|
name: "multiple common features",
|
|
legacyBytes: []byte{0xA2, 0x82},
|
|
expectedFeats: []lnwire.FeatureBit{1, 7, 9, 13, 15},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
features, err := deserializeChanEdgeFeatures(
|
|
tc.legacyBytes,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
for _, bit := range tc.expectedFeats {
|
|
require.True(t, features.IsSet(bit),
|
|
"expected bit %d to be set", bit)
|
|
}
|
|
|
|
// Verify no extra bits are set by creating expected
|
|
// feature vector and comparing.
|
|
expectedRaw := lnwire.NewRawFeatureVector(
|
|
tc.expectedFeats...,
|
|
)
|
|
require.True(t, expectedRaw.Equals(
|
|
features.RawFeatureVector),
|
|
"feature vectors don't match")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesNewFormat tests deserialization of
|
|
// feature bytes written in the new format (v0.20+), which contains
|
|
// a 2-byte big-endian length prefix followed by raw feature bits.
|
|
func TestDeserializeChanEdgeFeaturesNewFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
expectedFeats []lnwire.FeatureBit
|
|
}{
|
|
{
|
|
name: "empty features",
|
|
expectedFeats: nil,
|
|
},
|
|
{
|
|
name: "single feature bit 0",
|
|
expectedFeats: []lnwire.FeatureBit{0},
|
|
},
|
|
{
|
|
name: "single feature bit 15",
|
|
expectedFeats: []lnwire.FeatureBit{15},
|
|
},
|
|
{
|
|
name: "multiple features",
|
|
expectedFeats: []lnwire.FeatureBit{1, 5, 9, 13, 17},
|
|
},
|
|
{
|
|
name: "common lightning features",
|
|
expectedFeats: []lnwire.FeatureBit{
|
|
lnwire.DataLossProtectOptional,
|
|
lnwire.GossipQueriesOptional,
|
|
lnwire.TLVOnionPayloadOptional,
|
|
lnwire.StaticRemoteKeyOptional,
|
|
lnwire.PaymentAddrOptional,
|
|
},
|
|
},
|
|
{
|
|
name: "high bit features",
|
|
expectedFeats: []lnwire.FeatureBit{
|
|
lnwire.AMPOptional, // 31
|
|
lnwire.KeysendOptional, // 55
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create feature vector and encode in new format.
|
|
rawFeatures := lnwire.NewRawFeatureVector(
|
|
tc.expectedFeats...,
|
|
)
|
|
fv := lnwire.NewFeatureVector(
|
|
rawFeatures, lnwire.Features,
|
|
)
|
|
|
|
// Encode using the new format (with length prefix).
|
|
var buf bytes.Buffer
|
|
err := fv.Encode(&buf)
|
|
require.NoError(t, err)
|
|
|
|
// Deserialize and verify.
|
|
features, err := deserializeChanEdgeFeatures(
|
|
buf.Bytes(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
for _, bit := range tc.expectedFeats {
|
|
require.True(t, features.IsSet(bit),
|
|
"expected bit %d to be set", bit)
|
|
}
|
|
|
|
// Verify feature equality.
|
|
require.True(t, rawFeatures.Equals(
|
|
features.RawFeatureVector),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesFormatDetection tests that the format
|
|
// detection correctly distinguishes between legacy and new formats.
|
|
func TestDeserializeChanEdgeFeaturesFormatDetection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test that legacy format bytes that could theoretically be confused
|
|
// with new format are handled correctly. This shouldn't happen in
|
|
// practice because in legacy format the first byte always has at least
|
|
// one bit set (the highest feature bit determines the byte length).
|
|
|
|
// Create a feature vector with bit 8 set (requires 2 bytes in legacy).
|
|
// Legacy format: [0x01, 0x00] (big-endian, high byte first).
|
|
// As a length, 0x0100 = 256, which != 0 (len-2), so correctly detected
|
|
// as legacy.
|
|
legacyBit8 := []byte{0x01, 0x00}
|
|
features, err := deserializeChanEdgeFeatures(legacyBit8)
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsSet(8))
|
|
require.False(t, features.IsSet(0))
|
|
|
|
// New format with bit 8: [0x00, 0x02, 0x01, 0x00]
|
|
// Length prefix 0x0002 = 2, remaining 2 bytes = feature bits.
|
|
newFormatBit8 := []byte{0x00, 0x02, 0x01, 0x00}
|
|
features, err = deserializeChanEdgeFeatures(newFormatBit8)
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsSet(8))
|
|
require.False(t, features.IsSet(0))
|
|
|
|
// Test single byte legacy format - cannot be confused with new format
|
|
// since new format minimum is 2 bytes (the length prefix).
|
|
legacyBit0 := []byte{0x01}
|
|
features, err = deserializeChanEdgeFeatures(legacyBit0)
|
|
require.NoError(t, err)
|
|
require.True(t, features.IsSet(0))
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesRoundTrip tests that features can be
|
|
// serialized and deserialized correctly using the new format.
|
|
func TestDeserializeChanEdgeFeaturesRoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testFeatureSets := [][]lnwire.FeatureBit{
|
|
{},
|
|
{0},
|
|
{7},
|
|
{8},
|
|
{15},
|
|
{0, 1, 2, 3, 4, 5, 6, 7},
|
|
{8, 9, 10, 11, 12, 13, 14, 15},
|
|
{0, 8, 16, 24, 32},
|
|
{
|
|
lnwire.DataLossProtectOptional,
|
|
lnwire.GossipQueriesOptional,
|
|
lnwire.TLVOnionPayloadOptional,
|
|
lnwire.StaticRemoteKeyOptional,
|
|
lnwire.PaymentAddrOptional,
|
|
lnwire.MPPOptional,
|
|
lnwire.AnchorsZeroFeeHtlcTxOptional,
|
|
},
|
|
}
|
|
|
|
for _, featureBits := range testFeatureSets {
|
|
// Create and encode.
|
|
rawFeatures := lnwire.NewRawFeatureVector(featureBits...)
|
|
fv := lnwire.NewFeatureVector(rawFeatures, lnwire.Features)
|
|
|
|
var buf bytes.Buffer
|
|
err := fv.Encode(&buf)
|
|
require.NoError(t, err)
|
|
|
|
// Deserialize.
|
|
decoded, err := deserializeChanEdgeFeatures(buf.Bytes())
|
|
require.NoError(t, err)
|
|
|
|
// Verify equality.
|
|
require.True(t, rawFeatures.Equals(decoded.RawFeatureVector),
|
|
"mismatch for features %v", featureBits)
|
|
}
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesPropertyBased uses property-based testing
|
|
// to verify that the deserialization works correctly for arbitrary feature
|
|
// combinations in both legacy and new formats.
|
|
func TestDeserializeChanEdgeFeaturesPropertyBased(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test legacy format: raw feature bytes without length prefix.
|
|
rapid.Check(t, func(t *rapid.T) {
|
|
// Generate random feature bits (max 256 to keep reasonable).
|
|
numFeatures := rapid.IntRange(0, 20).Draw(t, "numFeatures")
|
|
featureBits := make([]lnwire.FeatureBit, numFeatures)
|
|
for i := 0; i < numFeatures; i++ {
|
|
featureBits[i] = lnwire.FeatureBit(
|
|
rapid.IntRange(0, 255).Draw(t, "featureBit"),
|
|
)
|
|
}
|
|
|
|
// Create feature vector.
|
|
rawFeatures := lnwire.NewRawFeatureVector(featureBits...)
|
|
|
|
// Encode without length prefix (legacy format).
|
|
var buf bytes.Buffer
|
|
err := rawFeatures.EncodeBase256(&buf)
|
|
require.NoError(t, err)
|
|
|
|
// Deserialize.
|
|
decoded, err := deserializeChanEdgeFeatures(buf.Bytes())
|
|
require.NoError(t, err)
|
|
|
|
// Verify equality.
|
|
require.True(t, rawFeatures.Equals(decoded.RawFeatureVector))
|
|
})
|
|
|
|
// Test new format: with length prefix.
|
|
rapid.Check(t, func(t *rapid.T) {
|
|
// Generate random feature bits.
|
|
numFeatures := rapid.IntRange(0, 20).Draw(t, "numFeatures")
|
|
featureBits := make([]lnwire.FeatureBit, numFeatures)
|
|
for i := 0; i < numFeatures; i++ {
|
|
featureBits[i] = lnwire.FeatureBit(
|
|
rapid.IntRange(0, 255).Draw(t, "featureBit"),
|
|
)
|
|
}
|
|
|
|
// Create feature vector.
|
|
rawFeatures := lnwire.NewRawFeatureVector(featureBits...)
|
|
fv := lnwire.NewFeatureVector(rawFeatures, lnwire.Features)
|
|
|
|
// Encode with length prefix (new format).
|
|
var buf bytes.Buffer
|
|
err := fv.Encode(&buf)
|
|
require.NoError(t, err)
|
|
|
|
// Deserialize.
|
|
decoded, err := deserializeChanEdgeFeatures(buf.Bytes())
|
|
require.NoError(t, err)
|
|
|
|
// Verify equality.
|
|
require.True(t, rawFeatures.Equals(decoded.RawFeatureVector))
|
|
})
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesLegacyFormatNoCollision verifies that
|
|
// the format detection cannot have false positives where legacy format
|
|
// bytes are incorrectly detected as new format.
|
|
func TestDeserializeChanEdgeFeaturesLegacyFormatNoCollision(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The detection works through canonical encoding verification.
|
|
// Even if a legacy vector accidentally matches the length check
|
|
// (e.g., a 258-byte vector starting with 0x01, 0x00), the decode/
|
|
// re-encode check will fail because the legacy encoding won't be
|
|
// canonical when interpreted as new format payload.
|
|
|
|
rapid.Check(t, func(t *rapid.T) {
|
|
// Generate feature bits with higher range to catch more edge
|
|
// cases, including vectors that could match the length check.
|
|
maxBit := rapid.IntRange(0, 2200).Draw(t, "maxBit")
|
|
numExtra := rapid.IntRange(0, 10).Draw(t, "numExtra")
|
|
|
|
featureBits := []lnwire.FeatureBit{lnwire.FeatureBit(maxBit)}
|
|
for i := 0; i < numExtra; i++ {
|
|
bit := rapid.IntRange(0, maxBit).Draw(t, "extraBit")
|
|
featureBits = append(featureBits,
|
|
lnwire.FeatureBit(bit))
|
|
}
|
|
|
|
rawFeatures := lnwire.NewRawFeatureVector(featureBits...)
|
|
|
|
// Encode in legacy format.
|
|
var buf bytes.Buffer
|
|
err := rawFeatures.EncodeBase256(&buf)
|
|
require.NoError(t, err)
|
|
|
|
legacyBytes := buf.Bytes()
|
|
if len(legacyBytes) < 2 {
|
|
// Single byte can't be confused with new format.
|
|
return
|
|
}
|
|
|
|
// Verify deserialization still works correctly regardless of
|
|
// whether the length check happens to match.
|
|
decoded, err := deserializeChanEdgeFeatures(legacyBytes)
|
|
require.NoError(t, err)
|
|
require.True(t, rawFeatures.Equals(decoded.RawFeatureVector),
|
|
"mismatch for legacy bytes %x with maxBit %d",
|
|
legacyBytes, maxBit)
|
|
})
|
|
}
|
|
|
|
// TestDeserializeChanEdgeFeaturesLengthCheckCollision specifically tests the
|
|
// edge case where a legacy feature vector accidentally satisfies the length
|
|
// check condition (first 2 bytes as uint16 == len - 2). This can happen with
|
|
// a 258-byte vector starting with 0x01, 0x00, where 256 == 258 - 2.
|
|
// The canonical encoding verification should correctly identify this as legacy
|
|
// format.
|
|
func TestDeserializeChanEdgeFeaturesLengthCheckCollision(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a legacy feature vector that will produce bytes where the
|
|
// first two bytes, interpreted as a length, equal len - 2.
|
|
//
|
|
// To get 258 bytes in legacy format, we need bit 2063 set (258*8-1).
|
|
// The first byte will be 0x01 (bit 2056 is in byte 0, and we need
|
|
// bit 2063 which is 0x80, but the bytes are big-endian so byte 0
|
|
// contains the high bits). Actually let's work this out:
|
|
//
|
|
// For 258 bytes, bits 2056-2063 are in byte 0.
|
|
// Setting bit 2056 gives byte[0] = 0x01.
|
|
// If byte[0] = 0x01 and byte[1] = 0x00, then as uint16 = 256 = 258-2.
|
|
//
|
|
// So we need: bit 2056 set (gives 0x01 in byte 0), and no bits in
|
|
// byte 1 set (bits 2048-2055), and at least one bit set below to
|
|
// ensure we have full 258 bytes (bit 0 to ensure byte 257 is non-zero
|
|
// won't work since it affects the last byte...).
|
|
//
|
|
// Actually the encoding is that the first byte contains the HIGHEST
|
|
// bits. So for 258 bytes:
|
|
// - byte[0] contains bits 2056-2063
|
|
// - byte[1] contains bits 2048-2055
|
|
// - ...
|
|
// - byte[257] contains bits 0-7
|
|
//
|
|
// To get byte[0] = 0x01 and byte[1] = 0x00:
|
|
// - Set bit 2056 (gives 0x01 in byte 0)
|
|
// - Don't set bits 2048-2055 (keeps byte 1 = 0x00)
|
|
//
|
|
// We also need to set some lower bit to have meaningful features.
|
|
featureBits := []lnwire.FeatureBit{
|
|
2056, // This gives 0x01 in first byte (258 bytes total)
|
|
0, // Set bit 0 for a meaningful feature
|
|
}
|
|
|
|
rawFeatures := lnwire.NewRawFeatureVector(featureBits...)
|
|
|
|
// Encode in legacy format.
|
|
var buf bytes.Buffer
|
|
err := rawFeatures.EncodeBase256(&buf)
|
|
require.NoError(t, err)
|
|
|
|
legacyBytes := buf.Bytes()
|
|
require.Len(t, legacyBytes, 258, "expected 258 bytes for bit 2056")
|
|
|
|
// Verify the collision condition: first 2 bytes as uint16 == len - 2.
|
|
encodedLen := binary.BigEndian.Uint16(legacyBytes[:2])
|
|
require.Equal(t, uint16(256), encodedLen,
|
|
"expected first 2 bytes to encode 256")
|
|
require.Equal(t, 256, len(legacyBytes)-2,
|
|
"expected length check to match")
|
|
|
|
// Despite the length check matching, deserialization should still
|
|
// correctly identify this as legacy format (via canonical encoding
|
|
// verification) and decode it properly.
|
|
decoded, err := deserializeChanEdgeFeatures(legacyBytes)
|
|
require.NoError(t, err)
|
|
require.True(t, decoded.IsSet(2056), "bit 2056 should be set")
|
|
require.True(t, decoded.IsSet(0), "bit 0 should be set")
|
|
require.True(t, rawFeatures.Equals(decoded.RawFeatureVector),
|
|
"feature vectors should match")
|
|
}
|