Merge pull request #10529 from Roasbeef/fix-edge-features-decode-compat
graphdb: fix backwards-compat for channel edge feature deserialization
This commit is contained in:
3 files changed
+530
-5
No files matched your search
@@ -62,6 +62,13 @@
|
|||||||
due to validation checks in the graph Builder that were resurfaced after the
|
due to validation checks in the graph Builder that were resurfaced after the
|
||||||
graph refactor work.
|
graph refactor work.
|
||||||
|
|
||||||
|
* [Fix backwards compatibility for channel edge feature
|
||||||
|
deserialization](https://github.com/lightningnetwork/lnd/pull/10529). Nodes
|
||||||
|
upgrading from pre-v0.20 versions could fail to read channel edges from their
|
||||||
|
graph database due to a format change in how channel features are serialized.
|
||||||
|
The fix adds automatic format detection to handle both legacy (raw feature
|
||||||
|
bits) and new (length-prefixed) formats.
|
||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
|
|
||||||
## Functional Enhancements
|
## Functional Enhancements
|
||||||
|
|||||||
+74
-5
@@ -4715,6 +4715,78 @@ func fetchChanEdgeInfo(edgeIndex kvdb.RBucket,
|
|||||||
return deserializeChanEdgeInfo(edgeInfoReader)
|
return deserializeChanEdgeInfo(edgeInfoReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deserializeChanEdgeFeatures deserializes channel edge features from bytes,
|
||||||
|
// handling both the legacy format (raw feature bits) and the current format
|
||||||
|
// (2-byte length prefix followed by feature bits).
|
||||||
|
//
|
||||||
|
// Legacy format (pre-v0.20): VarBytes containing raw feature bits directly.
|
||||||
|
// Current format (v0.20+): VarBytes containing a 2-byte big-endian length
|
||||||
|
// followed by the feature bits.
|
||||||
|
//
|
||||||
|
// The format is detected by checking if the first 2 bytes, interpreted as a
|
||||||
|
// big-endian uint16 length, equals len(featureBytes)-2. Since this length
|
||||||
|
// check alone could have false positives (e.g., a 258-byte legacy vector
|
||||||
|
// starting with 0x01, 0x00), we additionally verify by decoding and
|
||||||
|
// re-encoding the payload to confirm it produces the exact same bytes
|
||||||
|
// (canonical encoding check).
|
||||||
|
func deserializeChanEdgeFeatures(featureBytes []byte) (*lnwire.FeatureVector,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
features := lnwire.NewRawFeatureVector()
|
||||||
|
|
||||||
|
// Empty features are valid in both formats.
|
||||||
|
if len(featureBytes) == 0 {
|
||||||
|
return lnwire.NewFeatureVector(features, lnwire.Features), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this looks like the new format with a 2-byte length prefix.
|
||||||
|
// In the new format, the first 2 bytes encode the length of the
|
||||||
|
// remaining feature bytes.
|
||||||
|
if len(featureBytes) >= 2 {
|
||||||
|
encodedLen := binary.BigEndian.Uint16(featureBytes[:2])
|
||||||
|
if int(encodedLen) == len(featureBytes)-2 {
|
||||||
|
// This looks like it could be the new format. To be
|
||||||
|
// certain, we decode and re-encode to verify canonical
|
||||||
|
// encoding, as a legacy feature vector could
|
||||||
|
// accidentally match the length check (e.g., a 258-byte
|
||||||
|
// legacy vector starting with 0x01, 0x00 would have
|
||||||
|
// 256 == 258 - 2).
|
||||||
|
payload := featureBytes[2:]
|
||||||
|
tempFeatures := lnwire.NewRawFeatureVector()
|
||||||
|
err := tempFeatures.DecodeBase256(
|
||||||
|
bytes.NewReader(payload), int(encodedLen),
|
||||||
|
)
|
||||||
|
|
||||||
|
var checkBuf bytes.Buffer
|
||||||
|
if err == nil {
|
||||||
|
err = tempFeatures.EncodeBase256(&checkBuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were no errors and the re-encoded payload
|
||||||
|
// matches the original, we are confident it's the new
|
||||||
|
// format.
|
||||||
|
isCanonical := bytes.Equal(checkBuf.Bytes(), payload)
|
||||||
|
if err == nil && isCanonical {
|
||||||
|
return lnwire.NewFeatureVector(
|
||||||
|
tempFeatures, lnwire.Features,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy format: the bytes are raw feature bits without a length
|
||||||
|
// prefix.
|
||||||
|
err := features.DecodeBase256(
|
||||||
|
bytes.NewReader(featureBytes), len(featureBytes),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode features "+
|
||||||
|
"(legacy format): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnwire.NewFeatureVector(features, lnwire.Features), nil
|
||||||
|
}
|
||||||
|
|
||||||
func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) {
|
func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
@@ -4739,13 +4811,10 @@ func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
features := lnwire.NewRawFeatureVector()
|
edgeInfo.Features, err = deserializeChanEdgeFeatures(featureBytes)
|
||||||
err = features.Decode(bytes.NewReader(featureBytes))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to decode "+
|
return nil, err
|
||||||
"features: %w", err)
|
|
||||||
}
|
}
|
||||||
edgeInfo.Features = lnwire.NewFeatureVector(features, lnwire.Features)
|
|
||||||
|
|
||||||
proof := &models.ChannelAuthProof{}
|
proof := &models.ChannelAuthProof{}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
Reference in new issue
Block a user