Skip to content

Commit b9c71fd

Browse files
committed
Make init message serialization legacy-compatible
Make init message serialization give special treatment to the var_onion_optin feature flag. This flag (if it is present) will only be placed in the global_features section of the init message while other flags are only placed in the local_features section. This makes use compatible with legacy clients that distinguish between global and local features since var_onion_optin was the only global feature in the spec before the spec was changed and the two feature sets were merged. Some other miscellaneous changes are: Add a SetFeature method to the FeatureBit type. Remove the lazily-initialized mutable bytes field from FeatureBit since this made it possible for FeatureBit.ByteArray to become out of sync with FeatureBit.BitArray. ByteArray is now computed from the internal bitArray on demand. Modify the BitArray.ToByteArray extension method so that it is the inverse of the BitArray.FromBytes extension method, rather than the two methods ordering the bits within the bytes differently. BitArray.ToByteArray will also return the minimal-length array required to represent the BitArray rather than (possibly) containing 0x00 bytes at the front (which is a protocol violation if sent over the network as a feature set). This also means changing how Payment.TaggedField.WriteTo serializes feature bits to avoid padding being dropped when padding to a multiple of 5 bits in order to convert to Bech32. Change the BitArray.{FromBytes,ToByteArray} property test to take into account that zero bytes can be dropped from the front of the array. Add a test case which checks that converting a FeatureBit from and to a byte array correctly trims zero bytes at the start of the array, preserves the byte order of the array, and preserves the bit order within the bytes. FeatureBit.Equals now just converts both FeatureBits to byte arrays and tests those for equality. Since converting to bytes normalises the feature bits by removing excess zeros, this change fixes an issue where two FeatureBits of different lengths were considered unequal when the extra bits were all zeros.
1 parent 134bff1 commit b9c71fd

File tree

6 files changed

+140
-48
lines changed

6 files changed

+140
-48
lines changed

src/DotNetLightning.Core/Payment/PaymentRequest.fs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,11 @@ type TaggedField =
285285
let routeInfoBase32 = routeInfoBase256 |> Array.concat |> Helpers.convert8BitsTo5
286286
this.WriteField(writer, routeInfoBase32)
287287
| FeaturesTaggedField f ->
288-
if f.BitArray.Length = 0 then () else
289-
let pad = if (f.BitArray.Length % 40 = 0) then 0 else 40 - (f.BitArray.Length % 40)
290288
let dBase32 =
291-
let ba =
292-
[BitArray(pad, false); f.BitArray] |> BitArray.Concat |> fun baa -> baa.ToByteArray()
293-
ba |> Helpers.convert8BitsTo5
294-
|> Array.skipWhile((=)0uy)
289+
let mutable byteArray = f.BitArray.ToByteArray()
290+
while (byteArray.Length * 8) % 5 <> 0 do
291+
byteArray <- Array.concat [ [| 0uy |]; byteArray ]
292+
byteArray |> Helpers.convert8BitsTo5 |> Array.skipWhile((=)0uy)
295293
this.WriteField(writer, dBase32)
296294

297295

src/DotNetLightning.Core/Serialize/Features.fs

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ type Feature = private {
2929
with
3030
member this.MandatoryBitPosition = this.Mandatory
3131
member this.OptionalBitPosition = this.Mandatory + 1
32+
member this.BitPosition(support: FeaturesSupport) =
33+
match support with
34+
| Mandatory -> this.MandatoryBitPosition
35+
| Optional -> this.OptionalBitPosition
36+
3237
override this.ToString() = this.RfcName
3338

3439
static member OptionDataLossProtect = {
@@ -89,7 +94,7 @@ module internal Feature =
8994
|> Map.add (Feature.BasicMultiPartPayment) ([Feature.PaymentSecret])
9095
|> Map.add (Feature.PaymentSecret) ([Feature.VariableLengthOnion])
9196

92-
let private isFeatureOn(features: BitArray) (bit: int) =
97+
let isFeatureOn(features: BitArray) (bit: int) =
9398
(features.Length > bit) && features.Reverse().[bit]
9499

95100
let hasFeature(features: BitArray) (f: Feature) (support: FeaturesSupport option) =
@@ -170,17 +175,14 @@ module internal Feature =
170175
|> Set
171176

172177

173-
/// Uses regular class instead of F# type for caching byte[] representation
174178
[<StructuredFormatDisplay("{PrettyPrint}")>]
175179
type FeatureBit private (bitArray) =
176-
let mutable bytes = null
177180
member val BitArray: BitArray = bitArray with get, set
178181
member this.ByteArray
179182
with get() =
180-
if isNull bytes then
181-
bytes <- this.BitArray.ToByteArray()
182-
bytes
183-
and set(v: byte[]) = bytes <- v
183+
this.BitArray.ToByteArray()
184+
and set(bytes: byte[]) =
185+
this.BitArray <- BitArray.FromBytes(bytes)
184186
static member TryCreate(ba: BitArray) =
185187
result {
186188
do! Feature.validateFeatureGraph(ba)
@@ -196,12 +198,8 @@ type FeatureBit private (bitArray) =
196198
let b: bool array = [||]
197199
b |> BitArray |> FeatureBit
198200
static member TryCreate(bytes: byte[]) =
199-
result {
200-
let! fb = FeatureBit.TryCreate(BitArray.FromBytes(bytes))
201-
fb.ByteArray <- bytes
202-
return fb
203-
}
204-
201+
FeatureBit.TryCreate(BitArray.FromBytes(bytes))
202+
205203
static member TryCreate(v: int64) =
206204
BitArray.FromInt64(v) |> FeatureBit.TryCreate
207205

@@ -230,6 +228,27 @@ type FeatureBit private (bitArray) =
230228
override this.ToString() =
231229
this.BitArray.PrintBits()
232230

231+
member this.SetFeature(feature: Feature) (support: FeaturesSupport) (on: bool): unit =
232+
let index = feature.BitPosition support
233+
let length = this.BitArray.Length
234+
if length <= index then
235+
this.BitArray.Length <- index + 1
236+
237+
//this.BitArray.RightShift(index - length + 1)
238+
239+
// NOTE: Calling RightShift gives me:
240+
// "The field, constructor or member 'RightShift' is not defined."
241+
// So I just re-implement it here
242+
for i in (length - 1) .. -1 .. 0 do
243+
this.BitArray.[i + index - length + 1] <- this.BitArray.[i]
244+
245+
// NOTE: this probably wouldn't be necessary if we were using
246+
// RightShift, but the dotnet docs don't actualy specify that
247+
// RightShift sets the leading bits to zero.
248+
for i in 0 .. (index - length) do
249+
this.BitArray.[i] <- false
250+
this.BitArray.[this.BitArray.Length - index - 1] <- on
251+
233252
member this.HasFeature(f, ?featureType) =
234253
Feature.hasFeature this.BitArray (f) (featureType)
235254

@@ -249,12 +268,8 @@ type FeatureBit private (bitArray) =
249268

250269
// --- equality and comparison members ----
251270
member this.Equals(o: FeatureBit) =
252-
if this.BitArray.Length <> o.BitArray.Length then false else
253-
let mutable result = true
254-
for i in 0..this.BitArray.Length - 1 do
255-
if this.BitArray.[i] <> o.BitArray.[i] then
256-
result <- false
257-
result
271+
this.ByteArray = o.ByteArray
272+
258273
interface IEquatable<FeatureBit> with
259274
member this.Equals(o: FeatureBit) = this.Equals(o)
260275
override this.Equals(other: obj) =

src/DotNetLightning.Core/Serialize/Msgs/Msgs.fs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ open NBitcoin
99
open DotNetLightning.Utils
1010
open DotNetLightning.Utils.Primitives
1111
open DotNetLightning.Utils.OnionError
12+
open DotNetLightning.Core.Utils.Extensions
1213

1314
open DotNetLightning.Serialize
1415

@@ -368,7 +369,8 @@ type Init =
368369
interface ISetupMsg
369370
interface ILightningSerializable<Init> with
370371
member this.Deserialize(ls: LightningReaderStream) =
371-
// For backwards compatibility reason, we must consider legacy `global features` section. (see bolt 1)
372+
// For backwards compatibility reason, we must consider legacy
373+
// `global features` section. (see bolt 1)
372374
let globalFeatures = ls.ReadWithLen()
373375
let localFeatures = ls.ReadWithLen()
374376
let oredFeatures =
@@ -389,11 +391,54 @@ type Init =
389391
oredFeatures
390392
this.Features <- oredFeatures |> FeatureBit.CreateUnsafe
391393
this.TLVStream <- ls.ReadTLVStream() |> Array.map(InitTLV.FromGenericTLV)
394+
392395
member this.Serialize(ls) =
393-
// For backwards compatibility reason, we must consider legacy `global features` section. (see bolt 1)
394-
let g: byte[] = [||]
395-
ls.WriteWithLen(g)
396-
ls.WriteWithLen(this.Features.ToByteArray())
396+
// For legacy compatiblity we treat the VariableLengthOnion feature specially.
397+
// Older versions of the lightning protocol spec had a
398+
// distinction between global and local features, of which
399+
// VariableLengthOnion was the only global feature. Although
400+
// this distinction has since been removed, older clients still
401+
// expect VariableLengthOnion to appear only in the
402+
// global_features field of an init message and other features
403+
// to appear only in the local_features field. This is still
404+
// compatible with newer clients since those clients simply OR
405+
// the two feature fields.
406+
let localFeatures =
407+
let localFeatures = this.Features
408+
localFeatures.SetFeature
409+
Feature.VariableLengthOnion
410+
FeaturesSupport.Mandatory
411+
false
412+
localFeatures.SetFeature
413+
Feature.VariableLengthOnion
414+
FeaturesSupport.Optional
415+
false
416+
localFeatures.ToByteArray()
417+
let globalFeatures =
418+
let globalFeatures = FeatureBit.Zero
419+
let mandatory =
420+
this.Features.HasFeature(
421+
Feature.VariableLengthOnion,
422+
FeaturesSupport.Mandatory
423+
)
424+
let optional =
425+
this.Features.HasFeature(
426+
Feature.VariableLengthOnion,
427+
FeaturesSupport.Optional
428+
)
429+
if mandatory then
430+
globalFeatures.SetFeature
431+
Feature.VariableLengthOnion
432+
FeaturesSupport.Mandatory
433+
true
434+
if optional then
435+
globalFeatures.SetFeature
436+
Feature.VariableLengthOnion
437+
FeaturesSupport.Optional
438+
true
439+
globalFeatures.ToByteArray()
440+
ls.WriteWithLen(globalFeatures)
441+
ls.WriteWithLen(localFeatures)
397442
ls.WriteTLVStream(this.TLVStream |> Array.map(fun tlv -> tlv.ToGenericTLV()))
398443

399444
[<CLIMutable>]

src/DotNetLightning.Core/Utils/Extensions.fs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,34 @@ type BitArrayExtensions() =
101101
type System.Collections.BitArray with
102102
member this.ToByteArray() =
103103
if this.Length = 0 then [||] else
104-
let ret: byte[] = Array.zeroCreate (((this.Length - 1) / 8) + 1)
105-
let boolArray: bool[] = this.Reverse()
106-
let t = BitArray(boolArray)
107-
t.CopyTo(ret, 0)
108-
ret |> Array.rev
104+
105+
let leadingZeros =
106+
match (Seq.tryFindIndex (fun b -> b) (Seq.cast this)) with
107+
| Some i -> i
108+
| None -> this.Length
109+
let trueLength = this.Length - leadingZeros
110+
let desiredLength = ((trueLength + 7) / 8) * 8
111+
let difference = desiredLength - this.Length
112+
let bitArray =
113+
if difference < 0 then
114+
// Drop zeroes from the front of the array until we have a multiple of 8 bits
115+
let shortenedBitArray = BitArray(desiredLength)
116+
for i in 0 .. (desiredLength - 1) do
117+
shortenedBitArray.[i] <- this.[i - difference]
118+
shortenedBitArray
119+
else if difference > 0 then
120+
// Push zeroes to the front of the array until we have a multiple of 8 bits
121+
let lengthenedBitArray = BitArray(desiredLength)
122+
for i in 0 .. (this.Length - 1) do
123+
lengthenedBitArray.[i + difference] <- this.[i]
124+
lengthenedBitArray
125+
else
126+
this
127+
128+
// Copy the bit array to a byte array, then flip the bytes.
129+
let byteArray: byte[] = Array.zeroCreate(desiredLength / 8)
130+
bitArray.CopyTo(byteArray, 0)
131+
byteArray |> Array.map (fun b -> b.FlipBit())
109132

110133
static member From5BitEncoding(b: byte[]) =
111134
let bitArray = System.Collections.BitArray(b.Length * 5)

tests/DotNetLightning.Core.Tests/PaymentTests.fs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,17 @@ let tests =
172172
testCase "Please send $30 for coffee beans to the same peer, which supports features 9, 15 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111" <| fun _ ->
173173
let data = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu"
174174
let pr = PaymentRequest.Parse(data) |> Result.deref
175-
Expect.equal pr.PrefixValue "lnbc" ""
176-
Expect.equal pr.PaymentHash ("0001020304050607080900010203040506070809000102030405060708090102" |> uint256.Parse |> PaymentHash) ""
177-
Expect.equal (pr.TimestampValue.ToUnixTimeSeconds()) 1496314658L ""
178-
Expect.equal (pr.NodeIdValue) ("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad" |> hex.DecodeData |> PubKey |> NodeId) ""
179-
Expect.equal (pr.Description) (Choice1Of2 "coffee beans") ""
180-
Expect.isEmpty pr.FallbackAddresses ""
181-
Expect.isSome (pr.Features) ""
182-
Expect.isTrue(pr.Features.Value.HasFeature(Feature.PaymentSecret, Optional)) ""
183-
Expect.isTrue(pr.Features.Value.HasFeature(Feature.VariableLengthOnion, Optional)) ""
184-
Expect.equal (pr.ToString()) data ""
185-
Expect.equal (pr.ToString(msgSigner)) data ""
175+
Expect.equal pr.PrefixValue "lnbc" "pr.PrefixValue mismatch"
176+
Expect.equal pr.PaymentHash ("0001020304050607080900010203040506070809000102030405060708090102" |> uint256.Parse |> PaymentHash) "pr.PaymentHash mismatch"
177+
Expect.equal (pr.TimestampValue.ToUnixTimeSeconds()) 1496314658L "pr.TimestampValue mismatch"
178+
Expect.equal (pr.NodeIdValue) ("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad" |> hex.DecodeData |> PubKey |> NodeId) "pr.NodeIdValue mismatch"
179+
Expect.equal (pr.Description) (Choice1Of2 "coffee beans") "pr.Description mismatch"
180+
Expect.isEmpty pr.FallbackAddresses "pr.FallbackAddress mismatch"
181+
Expect.isSome (pr.Features) "pr.Features mismatch"
182+
Expect.isTrue(pr.Features.Value.HasFeature(Feature.PaymentSecret, Optional)) "pr.Features.Value (PaymentSecret) mismatch"
183+
Expect.isTrue(pr.Features.Value.HasFeature(Feature.VariableLengthOnion, Optional)) "pr.Features.Value (VariableLengthOnion) mismatch"
184+
Expect.equal (pr.ToString()) data "pr.ToString() mismatch"
185+
Expect.equal (pr.ToString(msgSigner)) data "pr.ToString(msgSigned) mismatch"
186186

187187
testCase "Same, but adding invalid unknown feature 100" <| fun _ ->
188188
let data = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"

tests/DotNetLightning.Core.Tests/Serialization.fs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,8 +742,19 @@ module SerializationTest =
742742
()
743743

744744
testProperty "BitArray serialization" <| fun (ba : NonNull<byte[]>) ->
745-
Expect.sequenceEqual (BitArray.FromBytes(ba.Get).ToByteArray()) (ba.Get) ""
746-
745+
let backAndForth = BitArray.FromBytes(ba.Get).ToByteArray()
746+
let finalArray = Array.zeroCreate ba.Get.Length
747+
Array.Copy(backAndForth, 0, finalArray, ba.Get.Length - backAndForth.Length, backAndForth.Length)
748+
Expect.equal ba.Get finalArray "BitArray.ToByteArray does not invert and trim BitArray.FromBytes"
749+
750+
testCase "FeatureBit to/from byte array preserves byte order, bit order and trims zero bytes" <| fun _ ->
751+
let features = FeatureBit.Zero
752+
features.ByteArray <- [| 0b00000000uy; 0b00100000uy; 0b10000010uy |]
753+
Expect.equal
754+
features.ByteArray
755+
[| 0b00100000uy; 0b10000010uy |]
756+
"unexpected ByteArray value"
757+
747758
testCase "features dependencies" <| fun _ ->
748759
let testCases =
749760
Map.empty

0 commit comments

Comments
 (0)