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); }