Skip to content

Commit

Permalink
Make taproot a bit easier
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Mar 1, 2024
1 parent 8e9c7c1 commit 36879c0
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 95 deletions.
57 changes: 28 additions & 29 deletions NBitcoin.Tests/TaprootBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public void TestVectorsCore()

var scriptWeights = new[]
{
(10u, Script.FromHex("51")),
(20u, Script.FromHex("52")),
(20u, Script.FromHex("53")),
(30u, Script.FromHex("54")),
(19u, Script.FromHex("55")),
(10u, Script.FromHex("51").ToTapScript(TapLeafVersion.C0)),
(20u, Script.FromHex("52").ToTapScript(TapLeafVersion.C0)),
(20u, Script.FromHex("53").ToTapScript(TapLeafVersion.C0)),
(30u, Script.FromHex("54").ToTapScript(TapLeafVersion.C0)),
(19u, Script.FromHex("55").ToTapScript(TapLeafVersion.C0)),
};

var treeInfo = TaprootSpendInfo.WithHuffmanTree(internalKey, scriptWeights);
Expand All @@ -52,7 +52,7 @@ public void TestVectorsCore()
Assert.True(
treeInfo
.ScriptToMerkleProofMap!
.TryGetValue((Script.FromHex(script), (byte)TaprootConstants.TAPROOT_LEAF_TAPSCRIPT), out var scriptSet)
.TryGetValue(Script.FromHex(script).ToTapScript(TapLeafVersion.C0), out var scriptSet)
);
var actualLength = scriptSet[0];
Assert.Equal(expectedLength, actualLength.Count);
Expand All @@ -62,7 +62,7 @@ public void TestVectorsCore()

foreach (var (_, script) in scriptWeights)
{
var ctrlBlock = treeInfo.GetControlBlock(script, (byte)TaprootConstants.TAPROOT_LEAF_TAPSCRIPT);
var ctrlBlock = treeInfo.GetControlBlock(script);
Assert.True(ctrlBlock.VerifyTaprootCommitment(outputKey, script));
}
}
Expand All @@ -82,11 +82,11 @@ public void TaptreeBuilderTests()
// / \ / \
// A B C / \
// D E
var a = Script.FromHex("51");
var b = Script.FromHex("52");
var c = Script.FromHex("53");
var d = Script.FromHex("54");
var e = Script.FromHex("55");
var a = Script.FromHex("51").ToTapScript(TapLeafVersion.C0);
var b = Script.FromHex("52").ToTapScript(TapLeafVersion.C0);
var c = Script.FromHex("53").ToTapScript(TapLeafVersion.C0);
var d = Script.FromHex("54").ToTapScript(TapLeafVersion.C0);
var e = Script.FromHex("55").ToTapScript(TapLeafVersion.C0);
builder
.AddLeaf(2, a)
.AddLeaf(2, b)
Expand All @@ -100,12 +100,12 @@ public void TaptreeBuilderTests()
var outputKey = treeInfo.OutputPubKey;
foreach (var script in new[] { a, b, c, d, e })
{
var ctrlBlock = treeInfo.GetControlBlock(script, (byte)TaprootConstants.TAPROOT_LEAF_TAPSCRIPT);
var ctrlBlock = treeInfo.GetControlBlock(script);
Assert.True(ctrlBlock.VerifyTaprootCommitment(outputKey, script));
}
}

private TaprootBuilder ProcessScriptTrees(JToken v, TaprootBuilder builder, List<(Script, byte)> leaves,
private TaprootBuilder ProcessScriptTrees(JToken v, TaprootBuilder builder, List<TapScript> leaves,
uint depth)
{

Expand All @@ -119,10 +119,10 @@ public void TaptreeBuilderTests()
}
else
{
var script = Script.FromHex(v["script"].ToString());
var script = Script.FromHex(v["script"].ToString()).ToTapScript(TapLeafVersion.C0);
var ver = (byte)(ulong)v["leafVersion"];
leaves.Add((script, ver));
builder = builder.AddLeaf(depth, script, ver);
leaves.Add(script);
builder = builder.AddLeaf(depth, script);
}
return builder;
}
Expand All @@ -133,14 +133,13 @@ public void CanHandleMultipleProofForSameScript()
{
var internalKey = TaprootInternalPubKey.Parse("93c7378d96518a75448821c4f7c8f4bae7ce60f804d03d1f0628dd5dd0f5de51");
var data = new byte[] { 0x01 };
var sc = new Script(OpcodeType.OP_RETURN, Op.GetPushOp(data));
var version = (byte)TaprootConstants.TAPROOT_LEAF_TAPSCRIPT;
var nodeInfoA = TaprootNodeInfo.NewLeafWithVersion(sc, version);
var nodeInfoB = TaprootNodeInfo.NewLeafWithVersion(sc, version);
var sc = new Script(OpcodeType.OP_RETURN, Op.GetPushOp(data)).ToTapScript(TapLeafVersion.C0);
var nodeInfoA = TaprootNodeInfo.NewLeaf(sc);
var nodeInfoB = TaprootNodeInfo.NewLeaf(sc);
var rootNode = nodeInfoA + nodeInfoB;
var info = TaprootSpendInfo.FromNodeInfo(internalKey, rootNode);
Assert.Single(info.ScriptToMerkleProofMap);
Assert.True(info.ScriptToMerkleProofMap.TryGetValue((sc, version), out var proofs));
Assert.True(info.ScriptToMerkleProofMap.TryGetValue(sc, out var proofs));
Assert.Equal(2, proofs.Count);
}

Expand Down Expand Up @@ -168,16 +167,16 @@ public void BIP341Tests()
var leafHashes = arr["intermediary"]!["leafHashes"]!.ToArray();
var ctrlBlocks = arr["expected"]!["scriptPathControlBlocks"]!.ToArray();
var builder = new TaprootBuilder();
var leaves = new List<(Script, byte)>();
var leaves = new List<TapScript>();
builder = ProcessScriptTrees(scriptTree, builder, leaves, 0);
var spendInfo = builder.Finalize(internalKey);
foreach (var (i, (script, version)) in leaves.Select((v, i) => (i, v)))
foreach (var (i, script) in leaves.Select((v, i) => (i, v)))
{
var expectedLeafHash = new uint256(Encoders.Hex.DecodeData(leafHashes[i].ToString()));
var expectedControlBlockStr = ctrlBlocks[i].ToString();
var expectedControlBlock = ControlBlock.FromHex(expectedControlBlockStr);
var leafHash = script.TaprootLeafHash(version);
var ctrlBlock = spendInfo.GetControlBlock(script, version);
var leafHash = script.LeafHash;
var ctrlBlock = spendInfo.GetControlBlock(script);
Assert.Equal(expectedLeafHash, leafHash);
var ctrlStr = Encoders.Hex.EncodeData(ctrlBlock.ToBytes());
_testOutputHelper.WriteLine($"Control block str {ctrlStr}. expected {expectedControlBlockStr}");
Expand Down Expand Up @@ -210,9 +209,9 @@ public void ScriptPathSpendUnitTest1()
{
var builder = new TaprootBuilder();
builder
.AddLeaf(1, Script.FromHex("210203a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5ac"))
.AddLeaf(2, Script.FromHex("2102a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"))
.AddLeaf(2, Script.FromHex("210203a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5ac"));
.AddLeaf(1, Script.FromHex("210203a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5ac").ToTapScript(TapLeafVersion.C0))
.AddLeaf(2, Script.FromHex("2102a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac").ToTapScript(TapLeafVersion.C0))
.AddLeaf(2, Script.FromHex("210203a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5ac").ToTapScript(TapLeafVersion.C0));

var internalKey = TaprootInternalPubKey.Parse("03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5");
var info = builder.Finalize(internalKey);
Expand Down
3 changes: 3 additions & 0 deletions NBitcoin.Tests/transaction_tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3129,6 +3129,9 @@ public void CanParseTransaction()
[Fact]
public void Play()
{
var script = new Key().GetScriptPubKey(ScriptPubKeyType.Legacy);
var tapSript = script.ToTapScript(TapLeafVersion.C0);

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

The name 'TapLeafVersion' does not exist in the current context

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

'Script' does not contain a definition for 'ToTapScript' and no accessible extension method 'ToTapScript' accepting a first argument of type 'Script' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

The name 'TapLeafVersion' does not exist in the current context

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

'Script' does not contain a definition for 'ToTapScript' and no accessible extension method 'ToTapScript' accepting a first argument of type 'Script' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

The name 'TapLeafVersion' does not exist in the current context

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

'Script' does not contain a definition for 'ToTapScript' and no accessible extension method 'ToTapScript' accepting a first argument of type 'Script' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

The name 'TapLeafVersion' does not exist in the current context

Check failure on line 3133 in NBitcoin.Tests/transaction_tests.cs

View workflow job for this annotation

GitHub Actions / 6.0 with netstandard2.0

'Script' does not contain a definition for 'ToTapScript' and no accessible extension method 'ToTapScript' accepting a first argument of type 'Script' could be found (are you missing a using directive or an assembly reference?)

var aa = PSBT.Parse("cHNidP8BAHEBAAAAAa5DWRuSCbbha7kDIp/LMMEZCYyyX4S6cBp7zWulUa/MAQAAAAD/////AhEoKgQAAAAAFgAU7nHAKjqvWjNf/8RQqlA77gFfVZcALTEBAAAAABYAFJetm1OALTie8TP5CY3/moUsteKlAAAAAAABAR8ljFsFAAAAABYAFO5xwCo6r1ozX//EUKpQO+4BX1WXIgIC1S6EeEs43Kpiqww0O0noYaUxYubyjtkZJIDCLyZBbx1HMEQCIG2DB/kiJIemnd1io2FH5YfmYbaYoUs0Yx5rujhTrYYJAiAM5uVbmbELCKssXXeVjKeD7hggtghj2OZcTIezwgfoTAEiBgLVLoR4SzjcqmKrDDQ7SehhpTFi5vKO2RkkgMIvJkFvHRgDOcj3VAAAgAEAAIAAAACAAQAAAAEAAAAAIgIC1S6EeEs43Kpiqww0O0noYaUxYubyjtkZJIDCLyZBbx0YAznI91QAAIABAACAAAAAgAEAAAABAAAAAAA=", Altcoins.Groestlcoin.Instance.Testnet);
aa.AssertSanity();
var aaew = aa.CheckSanity();
Expand Down
27 changes: 3 additions & 24 deletions NBitcoin/Script.cs
Original file line number Diff line number Diff line change
Expand Up @@ -520,29 +520,6 @@ public int Length
}
}

#if HAS_SPAN
private uint256? _leafHash;
public uint256 TaprootV1LeafHash
{
get
{
if (_leafHash is not null)
return _leafHash;
_leafHash = TaprootLeafHash((byte)TaprootConstants.TAPROOT_LEAF_TAPSCRIPT);
return _leafHash;
}
}

internal uint256 TaprootLeafHash(byte version)
{
var hash = new HashStream { SingleSHA256 = true };
hash.InitializeTagged("TapLeaf");
hash.WriteByte(version);
var bs = new BitcoinStream(hash, true);
bs.ReadWrite(this);
return hash.GetHash();
}
#endif
/// <summary>
/// Extract the ScriptCode delimited by the codeSeparatorIndex th OP_CODESEPARATOR.
/// </summary>
Expand Down Expand Up @@ -905,7 +882,9 @@ public byte[] ToBytes(bool @unsafe)
}

public WitScript ToWitScript() => new WitScript(this);

#if HAS_SPAN
public TapScript ToTapScript(TapLeafVersion version) => new TapScript(this, version);
#endif
public byte[] ToCompressedBytes()
{
var compressor = new ScriptCompressor(this);
Expand Down
20 changes: 10 additions & 10 deletions NBitcoin/Scripting/OutputDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ internal class TreeNode
internal bool Done;
}

public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey outputKey, [MaybeNullWhen(false)]out List<Tuple<int, Script, byte>> result)
public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey outputKey, [MaybeNullWhen(false)]out List<Tuple<int, TapScript>> result)
{
result = null;
if (!outputKey.CheckTapTweak(info.InternalPubKey, info.MerkleRoot, info.OutputPubKey.OutputKeyParity))
throw new ArgumentException($"TapTweak mismatch (outputKey: {outputKey}, internalKey: {info.InternalPubKey}, merkleRoot: {info.MerkleRoot})");

result = new List<Tuple<int, Script, byte>>();
result = new List<Tuple<int, TapScript>>();
if (info.IsKeyPathOnlySpend)
return true;

Expand All @@ -106,7 +106,7 @@ public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey ou
{
foreach (var control in kv.Value)
{
var (script, leafVersion) = kv.Key;
var script = kv.Key;
var controlB = control.Select(h => h.ToBytes()).SelectMany(x => x).ToArray();
// Skip script records with invalid control block size.
if (controlB.Length % TaprootConstants.TAPROOT_CONTROL_NODE_SIZE != 0)
Expand All @@ -115,7 +115,7 @@ public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey ou
continue;
}

var leafHash = new TaprootScriptLeaf(script, leafVersion).LeafHash;
var leafHash = new TaprootScriptLeaf(script).LeafHash;

TreeNode node = root;
var levels = controlB.Length/ TaprootConstants.TAPROOT_CONTROL_NODE_SIZE;
Expand Down Expand Up @@ -163,7 +163,7 @@ public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey ou
if (node.Sub?.Item1 is not null) return false;
node.Explored = true;
node.Inner = false;
node.Leaf = new TaprootScriptLeaf(script, leafVersion);
node.Leaf = new TaprootScriptLeaf(script);
node.Hash = leafHash;
}
}
Expand Down Expand Up @@ -195,7 +195,7 @@ public static bool InferTaprootTree(this TaprootSpendInfo info, TaprootPubKey ou
}
else if (!node.Inner)
{
result.Add(new Tuple<int, Script, byte>((int)stack.Count - 1, node.Leaf!.Script, node.Leaf.Version));
result.Add(new Tuple<int, TapScript>((int)stack.Count - 1, node.Leaf!.Script));
node.Done = true;
stack.Pop();
}
Expand Down Expand Up @@ -544,7 +544,7 @@ internal bool TryGetSpendInfo(ISigningRepository repo, [MaybeNullWhen(false)] ou

foreach (var s in scripts)
{
builder.AddLeaf((uint)depth, s);
builder.AddLeaf((uint)depth, s.ToTapScript(TapLeafVersion.C0));
}
}
}
Expand Down Expand Up @@ -748,7 +748,7 @@ public static OutputDescriptor NewMulti(uint m, IEnumerable<PubKeyProvider> pks,
return false;
foreach (var s in subScripts)
{
builder.AddLeaf((uint)depth, s);
builder.AddLeaf((uint)depth, s.ToTapScript(TapLeafVersion.C0));
}
}
}
Expand Down Expand Up @@ -1018,10 +1018,10 @@ private static OutputDescriptor InferFromScript(Script sc, ISigningRepository re
bool ok = true;
var subScripts = new List<OutputDescriptor>();
var depths = new List<int>();
foreach (var (depth, script, leafVersion) in tree)
foreach (var (depth, script) in tree)
{
OutputDescriptor? subdesc =
(leafVersion == TaprootConstants.TAPROOT_LEAF_TAPSCRIPT)
(script.Version == TapLeafVersion.C0)
? InferFromScript(script, repo, network, ScriptContext.P2TR)
: null;
if (subdesc is null)
Expand Down
95 changes: 95 additions & 0 deletions NBitcoin/TapScript.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#nullable enable
#if HAS_SPAN
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin.Crypto;

namespace NBitcoin
{
public enum TapLeafVersion : byte
{
C0 = 0xC0,
}
public class TapScript
{
public Script Script { get; }
public TapLeafVersion Version { get; }
uint256? _LeafHash;
public uint256 LeafHash => _LeafHash ??= ComputeLeafHash(Script, Version);

internal static uint256 ComputeLeafHash(Script script, TapLeafVersion version)
{
var hash = new HashStream { SingleSHA256 = true };
hash.InitializeTagged("TapLeaf");
hash.WriteByte((byte)version);
var bs = new BitcoinStream(hash, true);
bs.ReadWrite(script);
return hash.GetHash();
}
public TapScript(Script script, TapLeafVersion version)
{
//Lazy<int>
if (script is null)
throw new ArgumentNullException(nameof(script));
this.Script = script;
this.Version = version;
}

public TapScript(TapScript script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
Script = script.Script;
Version = script.Version;
_LeafHash = script._LeafHash;
}

[return: NotNullIfNotNull("script")]
public static implicit operator Script?(TapScript? script)
{
if (script is null)
return null;
return script.Script;
}
public override string ToString()
{
return $"{(byte)Version:X2}: {Script}";
}


public override bool Equals(object? obj)
{
TapScript? item = obj as TapScript;
if (item is null)
return false;
if (this._LeafHash is not null && item._LeafHash is not null)
return _LeafHash.Equals(item._LeafHash);
return Script == item.Script && Version == item.Version;
}
public static bool operator ==(TapScript a, TapScript b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (a is null || b is null)
return false;
if (a._LeafHash is not null && b._LeafHash is not null)
return a._LeafHash == b._LeafHash;
return a.Script == b.Script && a.Version == b.Version;
}

public static bool operator !=(TapScript a, TapScript b)
{
return !(a == b);
}

public override int GetHashCode()
{
return HashCode.Combine(Script, Version);
}
}
}
#endif
Loading

0 comments on commit 36879c0

Please sign in to comment.