Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -1409,15 +1409,16 @@ public synchronized Sha256Hash hashForWitnessSignature(
* <p>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.</p>
*
* <p>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
* <p>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
* scripts that contain no executed OP_CODESEPARATOR.</p>
*
* @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.ANYONECANPAY_ALL.
*/
public synchronized Sha256Hash hashForTaprootSignature(
int inputIndex,
Expand All @@ -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.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);
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;
Expand All @@ -1463,16 +1465,17 @@ 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);

// 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]
Expand Down Expand Up @@ -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]
Expand Down
30 changes: 29 additions & 1 deletion core/src/test/java/org/bitcoinj/core/TransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ private static void assertTaprootSigHash(
inputIndex,
tapLeafHash,
parseTaprootPrevOuts(),
(byte) sigHashType.value
sigHashType.byteValue()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also require the Transaction.SigHash type in hashForTaprootSignature
Suggestion: link

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! b7c04ff

);

assertArrayEquals(sigHash.getBytes(), Hex.decode(expectedSigHashHex));
Expand Down Expand Up @@ -580,6 +580,20 @@ public void testSigHashTaprootDefault() {
);
}

@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,
Transaction.SigHash.ANYONECANPAY_ALL,
"494fcea1db6f47aea9325255ba8b95c9f336e72a02f734c9f0c733878c463f58"
);
}

@Test
public void testSigHashTaprootScriptPath() {
// Spending a tapscript leaf (BIP342 script path) of input 0 instead of the key path.
Expand Down Expand Up @@ -647,6 +661,20 @@ public void testSigHashTaprootScriptPathDefault() {
);
}

@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(),
Transaction.SigHash.ANYONECANPAY_ALL,
"29f572084d6730eacc50d4cd5cb42326c13d19a6d4cf20b3f434266115141395"
);
}

@Test
public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() {
Transaction tx = FakeTxBuilder.createFakeTx(UNITTEST);
Expand Down