From d4ed8d4daefe58cd256d6e3d2cef36ab4256090b Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Thu, 16 Apr 2026 08:39:56 +0200 Subject: [PATCH 1/3] fix: use correct locktime and version in Taproot sighash --- CHANGELOG.md | 1 + src/bitcoin/Transaction.mo | 6 ++- test/bitcoin/p2trKeyPathSigHash.test.mo | 52 ++++++++++++++++++++++ test/bitcoin/p2trScriptPathSigHash.test.mo | 30 +++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7b864..d9a97f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.0.0 +* Bugfix: Taproot sighash now uses actual transaction values instead of hardcoded locktime=0 and version=2 (#14) * Migrate code from `base` to `core` * *Breaking:* Remove `toBytes` function in `bitcoin/TxOutput.mo` (use class method instead) * *Breaking:* Add length assertions inside `Bech32.encode()` diff --git a/src/bitcoin/Transaction.mo b/src/bitcoin/Transaction.mo index 40a0653..9725664 100644 --- a/src/bitcoin/Transaction.mo +++ b/src/bitcoin/Transaction.mo @@ -265,10 +265,12 @@ module { let sighash_type : [Nat8] = [0x00]; let nVersion_buffer = VarArray.repeat(0, 4); - Common.writeLE32(nVersion_buffer, 0, 2); + Common.writeLE32(nVersion_buffer, 0, version); let nVersion = Array.fromVarArray(nVersion_buffer); - let nLockTime : [Nat8] = Array.fromVarArray(VarArray.repeat(0, 4)); + let nLockTime_buffer = VarArray.repeat(0, 4); + Common.writeLE32(nLockTime_buffer, 0, locktime); + let nLockTime : [Nat8] = Array.fromVarArray(nLockTime_buffer); let sha_prevouts : [Nat8] = Sha256.fromArray(#sha256, prevouts.flatten()).toArray(); let amounts_bytes = amounts.map( diff --git a/test/bitcoin/p2trKeyPathSigHash.test.mo b/test/bitcoin/p2trKeyPathSigHash.test.mo index fcde4c0..2fa6847 100644 --- a/test/bitcoin/p2trKeyPathSigHash.test.mo +++ b/test/bitcoin/p2trKeyPathSigHash.test.mo @@ -1,8 +1,12 @@ import Blob "mo:core/Blob"; import Nat "mo:core/Nat"; +import Nat32 "mo:core/Nat32"; +import VarArray "mo:core/VarArray"; import { expect; test } "mo:test"; +import Transaction "../../src/bitcoin/Transaction"; +import Witness "../../src/bitcoin/Witness"; import TestVectors "p2trTestVectors"; for (testCase in TestVectors.testCases().vals()) { @@ -20,3 +24,51 @@ for (testCase in TestVectors.testCases().vals()) { }, ); }; + +test( + "non-zero locktime changes sighash", + func() { + let testCase = TestVectors.testCases()[0]; + let txLocktime0 = testCase.transaction(); + let txLocktime42 = Transaction.Transaction( + TestVectors.version, + testCase.inputs(), + testCase.outputs(), + VarArray.repeat(Witness.EMPTY_WITNESS, testCase.numInputs), + 42, + ); + + let hash0 = txLocktime0.createTaprootKeySpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, + ); + let hash42 = txLocktime42.createTaprootKeySpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, + ); + + expect.blob(Blob.fromArray(hash0)).notEqual(Blob.fromArray(hash42)); + }, +); + +test( + "different version changes sighash", + func() { + let testCase = TestVectors.testCases()[0]; + let txVersion2 = testCase.transaction(); + let txVersion1 = Transaction.Transaction( + 1, + testCase.inputs(), + testCase.outputs(), + VarArray.repeat(Witness.EMPTY_WITNESS, testCase.numInputs), + 0, + ); + + let hash2 = txVersion2.createTaprootKeySpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, + ); + let hash1 = txVersion1.createTaprootKeySpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, + ); + + expect.blob(Blob.fromArray(hash2)).notEqual(Blob.fromArray(hash1)); + }, +); diff --git a/test/bitcoin/p2trScriptPathSigHash.test.mo b/test/bitcoin/p2trScriptPathSigHash.test.mo index 20b997e..37aec1a 100644 --- a/test/bitcoin/p2trScriptPathSigHash.test.mo +++ b/test/bitcoin/p2trScriptPathSigHash.test.mo @@ -1,8 +1,13 @@ import Blob "mo:core/Blob"; import Nat "mo:core/Nat"; +import Nat32 "mo:core/Nat32"; +import VarArray "mo:core/VarArray"; import { expect; test } "mo:test"; +import P2tr "../../src/bitcoin/P2tr"; +import Transaction "../../src/bitcoin/Transaction"; +import Witness "../../src/bitcoin/Witness"; import TestVectors "p2trTestVectors"; for (testCase in TestVectors.testCases().vals()) { @@ -20,3 +25,28 @@ for (testCase in TestVectors.testCases().vals()) { }, ); }; + +test( + "non-zero locktime changes script spend sighash", + func() { + let testCase = TestVectors.testCases()[0]; + let txLocktime0 = testCase.transaction(); + let txLocktime42 = Transaction.Transaction( + TestVectors.version, + testCase.inputs(), + testCase.outputs(), + VarArray.repeat(Witness.EMPTY_WITNESS, testCase.numInputs), + 42, + ); + + let leafHash = P2tr.leafHash(testCase.leafScript()); + let hash0 = txLocktime0.createTaprootScriptSpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, leafHash, + ); + let hash42 = txLocktime42.createTaprootScriptSpendSignatureHash( + testCase.amounts(), testCase.ownScript(), 0, leafHash, + ); + + expect.blob(Blob.fromArray(hash0)).notEqual(Blob.fromArray(hash42)); + }, +); From 917887e18d1333f399d2de78f8f25a35d18baa51 Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Thu, 16 Apr 2026 08:48:06 +0200 Subject: [PATCH 2/3] remove unused import --- test/bitcoin/p2trKeyPathSigHash.test.mo | 1 - test/bitcoin/p2trScriptPathSigHash.test.mo | 1 - 2 files changed, 2 deletions(-) diff --git a/test/bitcoin/p2trKeyPathSigHash.test.mo b/test/bitcoin/p2trKeyPathSigHash.test.mo index 2fa6847..4229402 100644 --- a/test/bitcoin/p2trKeyPathSigHash.test.mo +++ b/test/bitcoin/p2trKeyPathSigHash.test.mo @@ -1,6 +1,5 @@ import Blob "mo:core/Blob"; import Nat "mo:core/Nat"; -import Nat32 "mo:core/Nat32"; import VarArray "mo:core/VarArray"; import { expect; test } "mo:test"; diff --git a/test/bitcoin/p2trScriptPathSigHash.test.mo b/test/bitcoin/p2trScriptPathSigHash.test.mo index 37aec1a..c565413 100644 --- a/test/bitcoin/p2trScriptPathSigHash.test.mo +++ b/test/bitcoin/p2trScriptPathSigHash.test.mo @@ -1,6 +1,5 @@ import Blob "mo:core/Blob"; import Nat "mo:core/Nat"; -import Nat32 "mo:core/Nat32"; import VarArray "mo:core/VarArray"; import { expect; test } "mo:test"; From 49b268254b4fa09c475cc09557b567b37f3475be Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Thu, 16 Apr 2026 10:02:36 +0200 Subject: [PATCH 3/3] code style --- src/bitcoin/Transaction.mo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bitcoin/Transaction.mo b/src/bitcoin/Transaction.mo index 9725664..a103ca7 100644 --- a/src/bitcoin/Transaction.mo +++ b/src/bitcoin/Transaction.mo @@ -270,7 +270,7 @@ module { let nLockTime_buffer = VarArray.repeat(0, 4); Common.writeLE32(nLockTime_buffer, 0, locktime); - let nLockTime : [Nat8] = Array.fromVarArray(nLockTime_buffer); + let nLockTime = nLockTime_buffer.toArray(); let sha_prevouts : [Nat8] = Sha256.fromArray(#sha256, prevouts.flatten()).toArray(); let amounts_bytes = amounts.map(