Skip to content

Commit 54702d2

Browse files
committed
Add SquashBytes format to Bech32
1 parent b35b32a commit 54702d2

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

NBitcoin.Tests/Bech32Test.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,36 @@ public void DetectInvalidChecksum()
9393
}
9494
}
9595

96+
[Fact]
97+
public void GenericDataTests()
98+
{
99+
var bech32Test =
100+
"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
101+
var lnurlBech32 = Bech32Encoder.ExtractEncoderFromString(bech32Test);
102+
lnurlBech32.StrictLength = false;
103+
lnurlBech32.SquashBytes = true;
104+
var lnurlData = lnurlBech32.DecodeDataRaw(bech32Test, out var bech32EncodingType);
105+
Assert.Equal(Bech32EncodingType.BECH32, bech32EncodingType);
106+
Assert.Equal("https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df",
107+
Encoding.UTF8.GetString(lnurlData));
108+
var encoded = lnurlBech32.EncodeRaw(Encoding.UTF8.GetBytes("https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"), Bech32EncodingType.BECH32);
109+
Assert.Equal(bech32Test, encoded.ToUpperInvariant());
110+
111+
var bechm32Test =
112+
"tark1x0lm8hhr2wc6n6lyemtyh9rz8rg2ftpkfun46aca56kjg3ws0tsztfpuanaquxc6faedvjk3tax0575y6perapg3e95654pk8r4fjecs5fyd2";
113+
var arkBech32m = Bech32Encoder.ExtractEncoderFromString(bechm32Test);
114+
arkBech32m.StrictLength = false;
115+
arkBech32m.SquashBytes = true;
116+
var arkData = arkBech32m.DecodeDataRaw(bechm32Test, out var bech32mEncodingType);
117+
Assert.Equal(Bech32EncodingType.BECH32M, bech32mEncodingType);
118+
Assert.Equal(64, arkData.Length);
119+
120+
var key1 = arkData.Take(32).ToArray();
121+
var key2 = arkData.Skip(32).ToArray();
122+
Assert.Equal("33ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0", Encoders.Hex.EncodeData(key1));
123+
Assert.Equal("25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967", Encoders.Hex.EncodeData(key2));
124+
}
125+
96126
[Fact]
97127
public void ValidAddress()
98128
{

NBitcoin/DataEncoders/Bech32Encoder.cs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,13 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
419419
{
420420
if (encodingType == null)
421421
throw new ArgumentNullException(nameof(encodingType));
422+
#if HAS_SPAN
423+
if (SquashBytes)
424+
data = ByteSquasher(data, 8, 5).AsSpan();
425+
#else
426+
data = ByteSquasher(data, 8, 5);
427+
#endif
428+
422429
#if HAS_SPAN
423430
Span<byte> combined = _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 ? new byte[v] :
424431
stackalloc byte[v];
@@ -434,6 +441,7 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
434441
combinedOffset += _Hrp.Length;
435442
combined[combinedOffset] = 49;
436443
combinedOffset++;
444+
437445
#if HAS_SPAN
438446
data.CopyTo(combined.Slice(combinedOffset));
439447
#else
@@ -446,9 +454,11 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
446454
#else
447455
var checkSum = CreateChecksum(data, offset, count, encodingType);
448456
#endif
457+
449458
#if HAS_SPAN
450459
checkSum.CopyTo(combined.Slice(combinedOffset, 6));
451460
combinedOffset += 6;
461+
452462
for (int i = 0; i < data.Length + 6; i++)
453463
#else
454464
Array.Copy(checkSum, 0, combined, combinedOffset, 6);
@@ -486,6 +496,8 @@ public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType)
486496
return DecodeDataCore(encoded, out encodingType);
487497
}
488498
public bool StrictLength { get; set; } = true;
499+
public bool SquashBytes { get; set; } = false;
500+
489501
protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType)
490502
{
491503
if (encoded == null)
@@ -543,11 +555,60 @@ protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType e
543555
throw new Bech32FormatException($"Error in Bech32 string at {String.Join(",", error)}", error);
544556
}
545557
#if HAS_SPAN
546-
return data.Slice(0, data.Length - 6).ToArray();
558+
var arr = data.Slice(0, data.Length - 6).ToArray();
547559
#else
548-
return data.Take(data.Length - 6).ToArray();
560+
var arr = data.Take(data.Length - 6).ToArray();
549561
#endif
562+
if (SquashBytes)
563+
{
564+
arr = ByteSquasher(arr, 5, 8);
565+
if (arr is null)
566+
throw new FormatException("Invalid squashed bech32");
567+
}
568+
return arr;
569+
}
570+
#if HAS_SPAN
571+
private static byte[] ByteSquasher(ReadOnlySpan<byte> input, int inputWidth, int outputWidth)
572+
#else
573+
private static byte[] ByteSquasher(byte[] input, int inputWidth, int outputWidth)
574+
#endif
575+
{
576+
var bitstash = 0;
577+
var accumulator = 0;
578+
var output = new List<byte>();
579+
var maxOutputValue = (1 << outputWidth) - 1;
580+
581+
for (var i = 0; i < input.Length; i++)
582+
{
583+
var c = input[i];
584+
if (c >> inputWidth != 0)
585+
{
586+
return null;
587+
}
588+
589+
accumulator = (accumulator << inputWidth) | c;
590+
bitstash += inputWidth;
591+
while (bitstash >= outputWidth)
592+
{
593+
bitstash -= outputWidth;
594+
output.Add((byte)((accumulator >> bitstash) & maxOutputValue));
595+
}
596+
}
597+
598+
// pad if going from 8 to 5
599+
if (inputWidth == 8 && outputWidth == 5)
600+
{
601+
if (bitstash != 0) output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue));
602+
}
603+
else if (bitstash >= inputWidth || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0)
604+
{
605+
// no pad from 5 to 8 allowed
606+
return null;
607+
}
608+
609+
return output.ToArray();
550610
}
611+
551612
#if HAS_SPAN
552613
// We don't use this one, but old version of NBitcoin.Altcoins does, we prefer not causing problems if there is a mismatch of
553614
// assembly between NBitcoin.Altcoins and NBitcoin.

0 commit comments

Comments
 (0)