diff --git a/src/NLightning.Bolt11/Models/TaggedFields/FallbackAddressTaggedField.cs b/src/NLightning.Bolt11/Models/TaggedFields/FallbackAddressTaggedField.cs index 670adeff..94de8ae2 100644 --- a/src/NLightning.Bolt11/Models/TaggedFields/FallbackAddressTaggedField.cs +++ b/src/NLightning.Bolt11/Models/TaggedFields/FallbackAddressTaggedField.cs @@ -84,12 +84,12 @@ public bool IsValid() /// The BitReader to read from /// The length of the field /// The network type - /// The FallbackAddressTaggedField - /// Thrown when the address is unknown or invalid - internal static FallbackAddressTaggedField FromBitReader(BitReader bitReader, short length, - BitcoinNetwork bitcoinNetwork) + /// The FallbackAddressTaggedField, or null if the version is unknown (per BOLT 11, unknown versions should be skipped) + /// Thrown when the network is invalid + internal static FallbackAddressTaggedField? FromBitReader(BitReader bitReader, short length, + BitcoinNetwork bitcoinNetwork) { - // Get Address Type + // Get Address Type (witness version) var addressType = bitReader.ReadByteFromBits(5); var newLength = length - 1; @@ -102,7 +102,11 @@ internal static FallbackAddressTaggedField FromBitReader(BitReader bitReader, sh var network = Network.GetNetwork(bitcoinNetwork) ?? throw new ArgumentException("Network is unknown or invalid.", nameof(bitcoinNetwork)); - BitcoinAddress address = addressType switch + + // Per BOLT 11: "MUST skip over `f` fields that use an unknown `version`" + // Supported versions: 0 (P2WPKH/P2WSH), 17 (P2PKH), 18 (P2SH) + // Unknown versions (1-16 for future witness versions, 19-31 reserved) should be skipped + BitcoinAddress? address = addressType switch { // Witness P2WPKH 0 when data.Length == 20 => new WitKeyId(data).GetAddress(network), @@ -112,9 +116,10 @@ internal static FallbackAddressTaggedField FromBitReader(BitReader bitReader, sh 17 => new KeyId(data).GetAddress(network), // P2SH 18 => new ScriptId(data).GetAddress(network), - _ => throw new ArgumentException("Address is unknown or invalid.", nameof(bitReader)) + // Unknown version - skip per BOLT 11 spec + _ => null }; - return new FallbackAddressTaggedField(address); + return address is null ? null : new FallbackAddressTaggedField(address); } } \ No newline at end of file diff --git a/test/NLightning.Bolt11.Tests/Models/TaggedFields/FallbackAddressTaggedFieldTests.cs b/test/NLightning.Bolt11.Tests/Models/TaggedFields/FallbackAddressTaggedFieldTests.cs index 3ef0c1dc..1cae2f3d 100644 --- a/test/NLightning.Bolt11.Tests/Models/TaggedFields/FallbackAddressTaggedFieldTests.cs +++ b/test/NLightning.Bolt11.Tests/Models/TaggedFields/FallbackAddressTaggedFieldTests.cs @@ -117,6 +117,7 @@ public void FromBitReader_CreatesCorrectlyFromBitReader(string expectedAddress, var taggedField = FallbackAddressTaggedField.FromBitReader(bitReader, bitLength, BitcoinNetwork.Mainnet); // Assert + Assert.NotNull(taggedField); Assert.Equal(expectedAddress, taggedField.Value.ToString()); } @@ -132,15 +133,45 @@ public void IsValid_ReturnsTrue() Assert.True(taggedField.IsValid()); } + [Theory] + [InlineData(1)] // Witness v1 (Taproot) - not yet supported + [InlineData(2)] // Future witness version + [InlineData(16)] // Future witness version + [InlineData(19)] // Reserved per BOLT 11 + [InlineData(20)] // Reserved per BOLT 11 + [InlineData(31)] // Reserved per BOLT 11 (max 5-bit value) + public void FromBitReader_ReturnsNull_ForUnknownVersion(byte version) + { + // Arrange - Create data with the version in the first 5 bits + // Version is stored in 5 bits, followed by 20 bytes (160 bits) of address data + // Total: 5 + 160 = 165 bits = 33 5-bit units + var data = new byte[21]; // (165 bits + 7) / 8 = 21 bytes + data[0] = (byte)(version << 3); // Version in first 5 bits (shifted to align) + // Fill remaining with dummy address data + for (var i = 1; i < data.Length; i++) + data[i] = 0xAB; + + var bitReader = new BitReader(data); + + // Act + var result = FallbackAddressTaggedField.FromBitReader(bitReader, 33, BitcoinNetwork.Mainnet); + + // Assert - Per BOLT 11: "MUST skip over `f` fields that use an unknown `version`" + Assert.Null(result); + } + [Fact] - public void FromBitReader_ThrowsArgumentException_ForInvalidAddressType() + public void FromBitReader_ReturnsNull_ForVersion0WithInvalidDataLength() { - // Arrange - var invalidData = new byte[] { 255, 0x00, 0x00, 0x00 }; - var bitReader = new BitReader(invalidData); + // Arrange - Version 0 but with 15-byte data (not 20 or 32) + // This should return null since it's not a valid P2WPKH (20) or P2WSH (32) + var data = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }; + var bitReader = new BitReader(data); - // Act & Assert - Assert.Throws(() => FallbackAddressTaggedField.FromBitReader( - bitReader, 4, BitcoinNetwork.Mainnet)); + // Act + var result = FallbackAddressTaggedField.FromBitReader(bitReader, 13, BitcoinNetwork.Mainnet); + + // Assert + Assert.Null(result); } } \ No newline at end of file diff --git a/test/NLightning.Integration.Tests/BOLT11/TaggedFields/FallbackAddressTaggedFieldIntegrationTests.cs b/test/NLightning.Integration.Tests/BOLT11/TaggedFields/FallbackAddressTaggedFieldIntegrationTests.cs index 9e14ad8e..c63eeb5f 100644 --- a/test/NLightning.Integration.Tests/BOLT11/TaggedFields/FallbackAddressTaggedFieldIntegrationTests.cs +++ b/test/NLightning.Integration.Tests/BOLT11/TaggedFields/FallbackAddressTaggedFieldIntegrationTests.cs @@ -73,6 +73,7 @@ public void FromBitReader_Reads_From_Beginning_Of_Buffer(BitcoinAddress address) var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } @@ -86,6 +87,7 @@ public void FromBitReader_Reads_From_Middle_Of_Buffer_Unaligned(BitcoinAddress a var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } @@ -99,6 +101,7 @@ public void FromBitReader_Reads_From_Middle_Of_Buffer_Aligned(BitcoinAddress add var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } @@ -112,6 +115,7 @@ public void FromBitReader_Reads_Near_End_Of_Buffer_Unaligned(BitcoinAddress addr var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } @@ -125,6 +129,7 @@ public void FromBitReader_Reads_Near_End_Of_Buffer_Aligned(BitcoinAddress addres var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } @@ -138,6 +143,7 @@ public void FromBitReader_Reads_At_End_Of_Buffer(BitcoinAddress address) var parsed = FallbackAddressTaggedField.FromBitReader(reader, length, BitcoinNetwork.Mainnet); + Assert.NotNull(parsed); Assert.Equal(expected.ToString(), parsed.Value.ToString()); } } \ No newline at end of file