From b649fb2ba86efcb2395c0b5051639a0c88cf2dda Mon Sep 17 00:00:00 2001 From: dangershony Date: Sun, 10 May 2026 23:45:25 +0100 Subject: [PATCH] Fix ComputeTapTweak span aliasing that causes all-zero output key on .NET 10 ARM64 ComputeTapTweak reused the same Span tweak32 for WriteToSpan serialization (input), SHA256.Write (hash input), and GetHash (output). On .NET 10 ARM64 (Android), the JIT miscompiles this aliasing pattern, causing AddTweak to receive a corrupted tweak and return an all-zero ECXOnlyPubKey. Fix: use a separate stackalloc byte[32] buffer for serialization, keeping tweak32 exclusively for the GetHash output. Add regression test that verifies a known BIP341 test vector produces the expected non-zero output key. --- NBitcoin.Tests/TaprootBuilderTests.cs | 27 +++++++++++++++++++++++++++ NBitcoin/TaprootFullPubKey.cs | 13 +++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/NBitcoin.Tests/TaprootBuilderTests.cs b/NBitcoin.Tests/TaprootBuilderTests.cs index 32b61978f..343abd090 100644 --- a/NBitcoin.Tests/TaprootBuilderTests.cs +++ b/NBitcoin.Tests/TaprootBuilderTests.cs @@ -225,6 +225,33 @@ public void ScriptPathSpendUnitTest1() var expectedSpk = "5120003cdb72825a12ea62f5834f3c47f9bf48d58d27f5ad1e6576ac613b093125f3"; Assert.Equal( expectedSpk, spk.ToHex()); } + + [Fact] + [Trait("UnitTest", "UnitTest")] + public void ComputeTapTweak_DoesNotProduceAllZeroOutputKey() + { + // Regression test for .NET 10 ARM64 JIT span-aliasing bug. + // ComputeTapTweak previously reused the same Span for + // WriteToSpan serialization and GetHash output, which caused + // AddTweak to return an all-zero key on ARM64. + // BIP341 test vector: internal key with no script tree (key-path only). + var hex = Encoders.Hex; + var internalKey = TaprootInternalPubKey.Parse( + "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d"); + var fullPubKey = internalKey.GetTaprootFullPubKey(); + + var outputBytes = new byte[32]; + fullPubKey.OutputKey.pubkey.WriteToSpan(outputBytes); + + // Output key must not be all zeros + Assert.False(Array.TrueForAll(outputBytes, b => b == 0), + "TaprootFullPubKey output key is all zeros — ComputeTapTweak span aliasing bug"); + + // Expected output key for this internal key with null merkle root (BIP341) + Assert.Equal( + "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + hex.EncodeData(outputBytes)); + } #endif } } diff --git a/NBitcoin/TaprootFullPubKey.cs b/NBitcoin/TaprootFullPubKey.cs index dc8510703..841e729ac 100644 --- a/NBitcoin/TaprootFullPubKey.cs +++ b/NBitcoin/TaprootFullPubKey.cs @@ -33,14 +33,19 @@ private TaprootFullPubKey(ECXOnlyPubKey outputKey, bool outputParity, TaprootInt internal static void ComputeTapTweak(TaprootInternalPubKey internalKey, uint256? merkleRoot, Span tweak32) { + // Use a separate buffer for serialization to avoid reusing tweak32 + // as both scratch space and output. Reusing the same Span for + // WriteToSpan/ToBytes input and GetHash output triggers a .NET 10 + // ARM64 JIT miscompilation due to span aliasing. + Span buf = stackalloc byte[32]; using Secp256k1.SHA256 sha = new Secp256k1.SHA256(); sha.InitializeTagged("TapTweak"); - internalKey.pubkey.WriteToSpan(tweak32); - sha.Write(tweak32); + internalKey.pubkey.WriteToSpan(buf); + sha.Write(buf); if (merkleRoot is uint256) { - merkleRoot.ToBytes(tweak32); - sha.Write(tweak32); + merkleRoot.ToBytes(buf); + sha.Write(buf); } sha.GetHash(tweak32); }