Skip to content

Commit

Permalink
Add SquashBytes format to Bech32
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Jan 14, 2025
1 parent b35b32a commit 54702d2
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 2 deletions.
30 changes: 30 additions & 0 deletions NBitcoin.Tests/Bech32Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,36 @@ public void DetectInvalidChecksum()
}
}

[Fact]
public void GenericDataTests()
{
var bech32Test =
"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS";
var lnurlBech32 = Bech32Encoder.ExtractEncoderFromString(bech32Test);
lnurlBech32.StrictLength = false;
lnurlBech32.SquashBytes = true;
var lnurlData = lnurlBech32.DecodeDataRaw(bech32Test, out var bech32EncodingType);
Assert.Equal(Bech32EncodingType.BECH32, bech32EncodingType);
Assert.Equal("https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df",
Encoding.UTF8.GetString(lnurlData));
var encoded = lnurlBech32.EncodeRaw(Encoding.UTF8.GetBytes("https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"), Bech32EncodingType.BECH32);
Assert.Equal(bech32Test, encoded.ToUpperInvariant());

var bechm32Test =
"tark1x0lm8hhr2wc6n6lyemtyh9rz8rg2ftpkfun46aca56kjg3ws0tsztfpuanaquxc6faedvjk3tax0575y6perapg3e95654pk8r4fjecs5fyd2";
var arkBech32m = Bech32Encoder.ExtractEncoderFromString(bechm32Test);
arkBech32m.StrictLength = false;
arkBech32m.SquashBytes = true;
var arkData = arkBech32m.DecodeDataRaw(bechm32Test, out var bech32mEncodingType);
Assert.Equal(Bech32EncodingType.BECH32M, bech32mEncodingType);
Assert.Equal(64, arkData.Length);

var key1 = arkData.Take(32).ToArray();
var key2 = arkData.Skip(32).ToArray();
Assert.Equal("33ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0", Encoders.Hex.EncodeData(key1));
Assert.Equal("25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967", Encoders.Hex.EncodeData(key2));
}

[Fact]
public void ValidAddress()
{
Expand Down
65 changes: 63 additions & 2 deletions NBitcoin/DataEncoders/Bech32Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,13 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
{
if (encodingType == null)
throw new ArgumentNullException(nameof(encodingType));
#if HAS_SPAN
if (SquashBytes)
data = ByteSquasher(data, 8, 5).AsSpan();
#else
data = ByteSquasher(data, 8, 5);
#endif

#if HAS_SPAN
Span<byte> combined = _Hrp.Length + 1 + data.Length + 6 is int v && v > 256 ? new byte[v] :
stackalloc byte[v];
Expand All @@ -434,6 +441,7 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
combinedOffset += _Hrp.Length;
combined[combinedOffset] = 49;
combinedOffset++;

#if HAS_SPAN
data.CopyTo(combined.Slice(combinedOffset));
#else
Expand All @@ -446,9 +454,11 @@ public virtual string EncodeData(ReadOnlySpan<byte> data, Bech32EncodingType enc
#else
var checkSum = CreateChecksum(data, offset, count, encodingType);
#endif

#if HAS_SPAN
checkSum.CopyTo(combined.Slice(combinedOffset, 6));
combinedOffset += 6;

for (int i = 0; i < data.Length + 6; i++)
#else
Array.Copy(checkSum, 0, combined, combinedOffset, 6);
Expand Down Expand Up @@ -486,6 +496,8 @@ public byte[] DecodeDataRaw(string encoded, out Bech32EncodingType encodingType)
return DecodeDataCore(encoded, out encodingType);
}
public bool StrictLength { get; set; } = true;
public bool SquashBytes { get; set; } = false;

protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType encodingType)
{
if (encoded == null)
Expand Down Expand Up @@ -543,11 +555,60 @@ protected virtual byte[] DecodeDataCore(string encoded, out Bech32EncodingType e
throw new Bech32FormatException($"Error in Bech32 string at {String.Join(",", error)}", error);
}
#if HAS_SPAN
return data.Slice(0, data.Length - 6).ToArray();
var arr = data.Slice(0, data.Length - 6).ToArray();
#else
return data.Take(data.Length - 6).ToArray();
var arr = data.Take(data.Length - 6).ToArray();
#endif
if (SquashBytes)
{
arr = ByteSquasher(arr, 5, 8);
if (arr is null)
throw new FormatException("Invalid squashed bech32");
}
return arr;
}
#if HAS_SPAN
private static byte[] ByteSquasher(ReadOnlySpan<byte> input, int inputWidth, int outputWidth)
#else
private static byte[] ByteSquasher(byte[] input, int inputWidth, int outputWidth)
#endif
{
var bitstash = 0;
var accumulator = 0;
var output = new List<byte>();
var maxOutputValue = (1 << outputWidth) - 1;

for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (c >> inputWidth != 0)
{
return null;
}

accumulator = (accumulator << inputWidth) | c;
bitstash += inputWidth;
while (bitstash >= outputWidth)
{
bitstash -= outputWidth;
output.Add((byte)((accumulator >> bitstash) & maxOutputValue));
}
}

// pad if going from 8 to 5
if (inputWidth == 8 && outputWidth == 5)
{
if (bitstash != 0) output.Add((byte)((accumulator << (outputWidth - bitstash)) & maxOutputValue));
}
else if (bitstash >= inputWidth || ((accumulator << (outputWidth - bitstash)) & maxOutputValue) != 0)
{
// no pad from 5 to 8 allowed
return null;
}

return output.ToArray();
}

#if HAS_SPAN
// We don't use this one, but old version of NBitcoin.Altcoins does, we prefer not causing problems if there is a mismatch of
// assembly between NBitcoin.Altcoins and NBitcoin.
Expand Down

0 comments on commit 54702d2

Please sign in to comment.