From c966ec2e4286a02e1142483263a43655d1996314 Mon Sep 17 00:00:00 2001 From: fedemoletta Date: Mon, 27 Jul 2026 16:03:44 -0300 Subject: [PATCH 1/3] support SIGHASH_ANYONECANPAY in taproot sighash Accept ALL | ANYONECANPAY (0x81, BIP341) in hashForTaprootSignature: skip the four all-input midstate hashes and commit to the signed input's outpoint, spent output and sequence instead. Write the full sighash type byte into the message (the masked byte was only correct for types without the flag) and fix the sign of the anyone-can-pay term in the buffer size formula. ANYONECANPAY alone (0x80) stays rejected: it is not a valid taproot sighash type. Golden values from btcd v0.24.2 with txscript.SigHashAll|txscript.SigHashAnyOneCanPay. --- .../java/org/bitcoinj/core/Transaction.java | 37 ++++++++++++----- .../org/bitcoinj/core/TransactionTest.java | 40 ++++++++++++++++--- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index c70b98c3500..24466ecb39a 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -1409,15 +1409,16 @@ public synchronized Sha256Hash hashForWitnessSignature( *

Passing a null scriptCode produces a key-path (BIP341) sighash. Passing the 32-byte tapleaf hash of the * script being spent produces a script-path (BIP342) sighash, which additionally commits to that leaf.

* - *

Limitations: only SigHash.DEFAULT and SigHash.ALL without ANYONECANPAY are supported; SigHash.NONE, - * SigHash.SINGLE and the annex are not implemented. OP_CODESEPARATOR is not supported: the codesep_pos in the + *

Limitations: only SigHash.DEFAULT, SigHash.ALL and SigHash.ALL | SigHash.ANYONECANPAY are supported; + * SigHash.NONE, SigHash.SINGLE and the annex are not implemented. ANYONECANPAY alone (0x80) is not a valid + * taproot sighash type per BIP341, so it is rejected. OP_CODESEPARATOR is not supported: the codesep_pos in the * script-path extension is always committed as 0xffffffff, so this method produces a correct sighash only for * scripts that contain no executed OP_CODESEPARATOR.

* * @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input. * @param scriptCode the 32-byte tapleaf hash for a script-path spend, or null for a key-path spend. * @param prevOutputs the previous outputs being spent, one per input, in input order. - * @param sigHashType should be SigHash.DEFAULT or SigHash.ALL, and not ANYONECANPAY. + * @param sigHashType should be SigHash.DEFAULT, SigHash.ALL or SigHash.ALL | SigHash.ANYONECANPAY. */ public synchronized Sha256Hash hashForTaprootSignature( int inputIndex, @@ -1436,15 +1437,16 @@ public synchronized Sha256Hash hashForTaprootSignature( boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; boolean signAll = (basicSigHashType != SigHash.SINGLE.value) && (basicSigHashType != SigHash.NONE.value); - // Only SigHash.DEFAULT, SigHash.ALL and !anyoneCanPay supported for now + // Only SigHash.DEFAULT, SigHash.ALL and SigHash.ALL | SigHash.ANYONECANPAY supported for now. + // ANYONECANPAY alone (0x80) masks to DEFAULT | ANYONECANPAY, not a valid taproot type (BIP341). checkArgument(basicSigHashType == SigHash.DEFAULT.value || basicSigHashType == SigHash.ALL.value); - checkArgument(!anyoneCanPay); + checkArgument(!anyoneCanPay || basicSigHashType == SigHash.ALL.value); final boolean hasAnnex = false; // Reserved. Not used. // According to BIP 341: 175 - is_anyonecanpay * 49 - is_none * 32 + has_annex * 32 int byteArraySize = 175; if (anyoneCanPay) { - byteArraySize += 49; + byteArraySize -= 49; } if (basicSigHashType == SigHash.NONE.value) { byteArraySize += 32; @@ -1463,8 +1465,8 @@ public synchronized Sha256Hash hashForTaprootSignature( // Epoch [1] (not technically part of the message, but every use-case adds this prefix) bos.write(new byte[] { 0x00 }); - // SigHash type [1] - bos.write(new byte[] {(byte) basicSigHashType}); + // SigHash type [1]: the full type byte, including the ANYONECANPAY flag if set + bos.write(new byte[] { sigHashType }); // nVersion [4] uint32ToByteStreamLE(version, bos); @@ -1472,7 +1474,8 @@ public synchronized Sha256Hash hashForTaprootSignature( // nLockTime [4] uint32ToByteStreamLE(lockTime, bos); - // input data [128 per input] always included since we failed for anyoneCanPay + // input data [128]: midstate hashes over all inputs, skipped for anyoneCanPay, + // which commits to just the signed input below instead if (!anyoneCanPay) { // calcHashPrevOuts [32] @@ -1532,7 +1535,21 @@ public synchronized Sha256Hash hashForTaprootSignature( bos.write(new byte[] { (byte) (hasScriptPath ? 0x02 : 0x00) }); if (anyoneCanPay) { - // MISSING: commit to the spent output and sequence (never since we failed for anyoneCanPay) + final TransactionInput input = this.inputs.get(inputIndex); + final TransactionOutput prevOutput = prevOutputs.get(inputIndex); + final byte[] prevOutScriptBytes = prevOutput.getScriptBytes(); + + // Outpoint [36] + bos.write(input.getOutpoint().getHash().getReversedBytes()); + uint32ToByteStreamLE(input.getOutpoint().getIndex(), bos); + + // Spent output: amount [8] and scriptPubKey [var, with size prefix] + int64ToByteStreamLE(prevOutput.getValue().value, bos); + bos.write(new VarInt(prevOutScriptBytes.length).encode()); + bos.write(prevOutScriptBytes); + + // nSequence [4] + uint32ToByteStreamLE(input.getSequenceNumber(), bos); } else { // Input index [4] diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index a1e7eb2fe97..2639cdeb965 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -540,7 +540,7 @@ private static byte[] taprootTestTapLeafHash() { private static void assertTaprootSigHash( int inputIndex, byte[] tapLeafHash, - Transaction.SigHash sigHashType, + byte sigHashType, String expectedSigHashHex ) { final Transaction tx = new Transaction(RegTestParams.get(), Hex.decode(TAPROOT_TX_HEX)); @@ -549,7 +549,7 @@ private static void assertTaprootSigHash( inputIndex, tapLeafHash, parseTaprootPrevOuts(), - (byte) sigHashType.value + sigHashType ); assertArrayEquals(sigHash.getBytes(), Hex.decode(expectedSigHashHex)); @@ -562,7 +562,7 @@ public void testSigHashTaproot() { assertTaprootSigHash( 1, null, - Transaction.SigHash.ALL, + (byte) Transaction.SigHash.ALL.value, "626ab955d58c9a8a600a0c580549d06dc7da4e802eb2a531f62a588e430967a8" ); } @@ -575,11 +575,25 @@ public void testSigHashTaprootDefault() { assertTaprootSigHash( 1, null, - Transaction.SigHash.DEFAULT, + (byte) Transaction.SigHash.DEFAULT.value, "92ac7cead5a678703fd3672b46d224d2bbca88d8c706ebf9f93bf60951726328" ); } + @Test + public void testSigHashTaprootAnyoneCanPay() { + // Same key-path spend as testSigHashTaproot, signed with SIGHASH_ALL | SIGHASH_ANYONECANPAY + // (0x81) instead of ALL. Golden value from btcd v0.24.2: keyPathExpectedSigHash in the + // main.go snippet in testSigHashTaprootScriptPath, with + // txscript.SigHashAll|txscript.SigHashAnyOneCanPay instead of txscript.SigHashAll. + assertTaprootSigHash( + 1, + null, + (byte) (Transaction.SigHash.ALL.value | Transaction.SigHash.ANYONECANPAY.value), + "494fcea1db6f47aea9325255ba8b95c9f336e72a02f734c9f0c733878c463f58" + ); + } + @Test public void testSigHashTaprootScriptPath() { // Spending a tapscript leaf (BIP342 script path) of input 0 instead of the key path. @@ -629,7 +643,7 @@ public void testSigHashTaprootScriptPath() { assertTaprootSigHash( 0, taprootTestTapLeafHash(), - Transaction.SigHash.ALL, + (byte) Transaction.SigHash.ALL.value, "81072ecd5edaa818671662c9318a610d0fd5bada8e7951f6d8d216918b9e21e0" ); } @@ -642,11 +656,25 @@ public void testSigHashTaprootScriptPathDefault() { assertTaprootSigHash( 0, taprootTestTapLeafHash(), - Transaction.SigHash.DEFAULT, + (byte) Transaction.SigHash.DEFAULT.value, "fc6011b62bd02db1ef43f727925e850216657d1d8da2fba28e927f03f9f4ad9a" ); } + @Test + public void testSigHashTaprootScriptPathAnyoneCanPay() { + // Same tapscript leaf spend as testSigHashTaprootScriptPath, signed with + // SIGHASH_ALL | SIGHASH_ANYONECANPAY (0x81) instead of ALL. Golden value from btcd v0.24.2: + // the main.go snippet in testSigHashTaprootScriptPath, with + // txscript.SigHashAll|txscript.SigHashAnyOneCanPay instead of txscript.SigHashAll. + assertTaprootSigHash( + 0, + taprootTestTapLeafHash(), + (byte) (Transaction.SigHash.ALL.value | Transaction.SigHash.ANYONECANPAY.value), + "29f572084d6730eacc50d4cd5cb42326c13d19a6d4cf20b3f434266115141395" + ); + } + @Test public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() { Transaction tx = FakeTxBuilder.createFakeTx(UNITTEST); From c19d01a405b579a51dc1db8567afa5e8db7f4728 Mon Sep 17 00:00:00 2001 From: fedemoletta Date: Tue, 28 Jul 2026 12:12:03 -0300 Subject: [PATCH 2/3] use ANYONECANPAY_ALL instead of composing sighash flags Replace SigHash.ALL | SigHash.ANYONECANPAY with the existing ANYONECANPAY_ALL enum constant in javadoc, comments and tests. The test helper takes Transaction.SigHash again and converts with byteValue(), so call sites pass plain enum constants without casts. No behavior change. --- .../main/java/org/bitcoinj/core/Transaction.java | 6 +++--- .../java/org/bitcoinj/core/TransactionTest.java | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index 24466ecb39a..4fdcd73a198 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -1409,7 +1409,7 @@ public synchronized Sha256Hash hashForWitnessSignature( *

Passing a null scriptCode produces a key-path (BIP341) sighash. Passing the 32-byte tapleaf hash of the * script being spent produces a script-path (BIP342) sighash, which additionally commits to that leaf.

* - *

Limitations: only SigHash.DEFAULT, SigHash.ALL and SigHash.ALL | SigHash.ANYONECANPAY are supported; + *

Limitations: only SigHash.DEFAULT, SigHash.ALL and SigHash.ANYONECANPAY_ALL are supported; * SigHash.NONE, SigHash.SINGLE and the annex are not implemented. ANYONECANPAY alone (0x80) is not a valid * taproot sighash type per BIP341, so it is rejected. OP_CODESEPARATOR is not supported: the codesep_pos in the * script-path extension is always committed as 0xffffffff, so this method produces a correct sighash only for @@ -1418,7 +1418,7 @@ public synchronized Sha256Hash hashForWitnessSignature( * @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input. * @param scriptCode the 32-byte tapleaf hash for a script-path spend, or null for a key-path spend. * @param prevOutputs the previous outputs being spent, one per input, in input order. - * @param sigHashType should be SigHash.DEFAULT, SigHash.ALL or SigHash.ALL | SigHash.ANYONECANPAY. + * @param sigHashType should be SigHash.DEFAULT, SigHash.ALL or SigHash.ANYONECANPAY_ALL. */ public synchronized Sha256Hash hashForTaprootSignature( int inputIndex, @@ -1437,7 +1437,7 @@ public synchronized Sha256Hash hashForTaprootSignature( boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; boolean signAll = (basicSigHashType != SigHash.SINGLE.value) && (basicSigHashType != SigHash.NONE.value); - // Only SigHash.DEFAULT, SigHash.ALL and SigHash.ALL | SigHash.ANYONECANPAY supported for now. + // Only SigHash.DEFAULT, SigHash.ALL and SigHash.ANYONECANPAY_ALL supported for now. // ANYONECANPAY alone (0x80) masks to DEFAULT | ANYONECANPAY, not a valid taproot type (BIP341). checkArgument(basicSigHashType == SigHash.DEFAULT.value || basicSigHashType == SigHash.ALL.value); checkArgument(!anyoneCanPay || basicSigHashType == SigHash.ALL.value); diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index 2639cdeb965..eb7dc99fd85 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -540,7 +540,7 @@ private static byte[] taprootTestTapLeafHash() { private static void assertTaprootSigHash( int inputIndex, byte[] tapLeafHash, - byte sigHashType, + Transaction.SigHash sigHashType, String expectedSigHashHex ) { final Transaction tx = new Transaction(RegTestParams.get(), Hex.decode(TAPROOT_TX_HEX)); @@ -549,7 +549,7 @@ private static void assertTaprootSigHash( inputIndex, tapLeafHash, parseTaprootPrevOuts(), - sigHashType + sigHashType.byteValue() ); assertArrayEquals(sigHash.getBytes(), Hex.decode(expectedSigHashHex)); @@ -562,7 +562,7 @@ public void testSigHashTaproot() { assertTaprootSigHash( 1, null, - (byte) Transaction.SigHash.ALL.value, + Transaction.SigHash.ALL, "626ab955d58c9a8a600a0c580549d06dc7da4e802eb2a531f62a588e430967a8" ); } @@ -575,7 +575,7 @@ public void testSigHashTaprootDefault() { assertTaprootSigHash( 1, null, - (byte) Transaction.SigHash.DEFAULT.value, + Transaction.SigHash.DEFAULT, "92ac7cead5a678703fd3672b46d224d2bbca88d8c706ebf9f93bf60951726328" ); } @@ -589,7 +589,7 @@ public void testSigHashTaprootAnyoneCanPay() { assertTaprootSigHash( 1, null, - (byte) (Transaction.SigHash.ALL.value | Transaction.SigHash.ANYONECANPAY.value), + Transaction.SigHash.ANYONECANPAY_ALL, "494fcea1db6f47aea9325255ba8b95c9f336e72a02f734c9f0c733878c463f58" ); } @@ -643,7 +643,7 @@ public void testSigHashTaprootScriptPath() { assertTaprootSigHash( 0, taprootTestTapLeafHash(), - (byte) Transaction.SigHash.ALL.value, + Transaction.SigHash.ALL, "81072ecd5edaa818671662c9318a610d0fd5bada8e7951f6d8d216918b9e21e0" ); } @@ -656,7 +656,7 @@ public void testSigHashTaprootScriptPathDefault() { assertTaprootSigHash( 0, taprootTestTapLeafHash(), - (byte) Transaction.SigHash.DEFAULT.value, + Transaction.SigHash.DEFAULT, "fc6011b62bd02db1ef43f727925e850216657d1d8da2fba28e927f03f9f4ad9a" ); } @@ -670,7 +670,7 @@ public void testSigHashTaprootScriptPathAnyoneCanPay() { assertTaprootSigHash( 0, taprootTestTapLeafHash(), - (byte) (Transaction.SigHash.ALL.value | Transaction.SigHash.ANYONECANPAY.value), + Transaction.SigHash.ANYONECANPAY_ALL, "29f572084d6730eacc50d4cd5cb42326c13d19a6d4cf20b3f434266115141395" ); } From b7c04ff03a7074c33636e5948083744eb35e4d23 Mon Sep 17 00:00:00 2001 From: fedemoletta Date: Tue, 28 Jul 2026 15:06:04 -0300 Subject: [PATCH 3/3] take SigHash instead of raw byte in hashForTaprootSignature --- core/src/main/java/org/bitcoinj/core/Transaction.java | 9 +++++---- .../src/test/java/org/bitcoinj/core/TransactionTest.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index 4fdcd73a198..720b1b356bb 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -1424,7 +1424,7 @@ public synchronized Sha256Hash hashForTaprootSignature( int inputIndex, byte[] scriptCode, List prevOutputs, - byte sigHashType){ + SigHash sigHashType){ // A non-null scriptCode is the 32-byte tapleaf hash of the script being spent (BIP342 // script path). Null means a key-path spend (BIP341). @@ -1433,8 +1433,9 @@ public synchronized Sha256Hash hashForTaprootSignature( checkArgument(inputIndex < prevOutputs.size()); checkArgument(prevOutputs.size() == inputs.size()); - int basicSigHashType = sigHashType & 0x1f; - boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; + final byte sigHashTypeByte = sigHashType.byteValue(); + int basicSigHashType = sigHashTypeByte & 0x1f; + boolean anyoneCanPay = (sigHashTypeByte & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; boolean signAll = (basicSigHashType != SigHash.SINGLE.value) && (basicSigHashType != SigHash.NONE.value); // Only SigHash.DEFAULT, SigHash.ALL and SigHash.ANYONECANPAY_ALL supported for now. @@ -1466,7 +1467,7 @@ public synchronized Sha256Hash hashForTaprootSignature( bos.write(new byte[] { 0x00 }); // SigHash type [1]: the full type byte, including the ANYONECANPAY flag if set - bos.write(new byte[] { sigHashType }); + bos.write(new byte[] { sigHashTypeByte }); // nVersion [4] uint32ToByteStreamLE(version, bos); diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index eb7dc99fd85..c5f72c7919e 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -549,7 +549,7 @@ private static void assertTaprootSigHash( inputIndex, tapLeafHash, parseTaprootPrevOuts(), - sigHashType.byteValue() + sigHashType ); assertArrayEquals(sigHash.getBytes(), Hex.decode(expectedSigHashHex));