Skip to content

Commit 7f271b2

Browse files
authored
Merge pull request joemphilips#92 from canndrew/init-message-legacy-compat
Init message legacy compatibility
2 parents 134bff1 + b892974 commit 7f271b2

File tree

6 files changed

+144
-53
lines changed

6 files changed

+144
-53
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: 32 additions & 10 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)
@@ -125,11 +148,10 @@ type System.Collections.BitArray with
125148
BitArray.From5BitEncoding(b)
126149

127150
member this.Reverse() =
128-
let length = this.Length
129-
let mutable result = Array.zeroCreate length
130-
for i in 0 .. (length - 1) do
131-
result.[i] <- this.[length - i - 1]
132-
result
151+
let boolArray: array<bool> = Array.ofSeq (Seq.cast this)
152+
Array.Reverse boolArray
153+
BitArray(boolArray)
154+
133155
member this.PrintBits() =
134156
let sb = StringBuilder()
135157
for b in this do

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)