Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
17 changes: 12 additions & 5 deletions bitcoinutils/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,8 +843,6 @@ def get_transaction_segwit_digest(

return hashlib.sha256(hashlib.sha256(tx_for_signing).digest()).digest()

# TODO Update doc with TAPROOT_SIGHASH_ALL
# clean prints after finishing other sighashes
def get_transaction_taproot_digest(
self,
txin_index: int,
Expand All @@ -854,6 +852,7 @@ def get_transaction_taproot_digest(
script=Script([]),
leaf_ver=LEAF_VERSION_TAPSCRIPT,
sighash=TAPROOT_SIGHASH_ALL,
annex: Optional[bytes] = None,
):
"""Returns the segwit v1 (taproot) transaction's digest for signing.
https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
Expand Down Expand Up @@ -886,6 +885,8 @@ def get_transaction_taproot_digest(
The script version, LEAF_VERSION_TAPSCRIPT for the default tapscript
sighash : int
The type of the signature hash to be created
annex : bytes, optional
Optional annex data (will be hashed as per BIP-341)
"""

# clone transaction to modify without messing up the real transaction
Expand Down Expand Up @@ -962,7 +963,9 @@ def get_transaction_taproot_digest(
tx_for_signing += hash_outputs

# Data about this input
spend_type = ext_flag * 2 + 0 # 0 for hard-coded - no annex_present
# Set annex_present flag in spend_type if annex is present
annex_present = 1 if annex is not None else 0
spend_type = ext_flag * 2 + annex_present

tx_for_signing += bytes([spend_type])

Expand All @@ -986,8 +989,12 @@ def get_transaction_taproot_digest(
# print('4')
tx_for_signing += txin_index.to_bytes(4, "little")

# TODO if annex is present it should be added here
# length of annex should use compact_size
# Add annex if present
if annex_present:
# Encode annex with a TapLeaf prefix of 0x50 as per BIP-341
annex_bytes = b'\x50' + encode_varint(len(annex)) + annex
# Hash the annex and add to the signature message
tx_for_signing += hashlib.sha256(annex_bytes).digest()

# Data about this output
if sighash_single:
Expand Down
32 changes: 32 additions & 0 deletions docs/signature_hash_annex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Signature Hash Annex Support

## Overview

This feature implements support for the signature hash annex as defined in [BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) (Taproot). The annex is an additional data structure that can be included in the transaction signature hash calculation, allowing for future extensions to the signature validation system.

## Implementation Details

The annex is an optional parameter in the `get_transaction_taproot_digest` method. When provided, it changes how the transaction's signature hash is calculated according to the BIP-341 specification:

1. The `spend_type` byte includes bit 0 set to 1 to indicate the presence of an annex
2. The annex is prefixed with 0x50 and its length as a compact size
3. The SHA256 hash of this prefixed annex is included in the signature hash calculation

## Usage

```python
from bitcoinutils.transactions import Transaction
from bitcoinutils.utils import h_to_b

# Your existing code to create and set up a transaction
# ...

# Calculate signature hash with annex
annex_data = h_to_b("aabbccdd") # Your annex data as bytes
signature_hash = tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=script_pubkeys,
amounts=amounts,
sighash=TAPROOT_SIGHASH_ALL,
annex=annex_data
)
159 changes: 159 additions & 0 deletions tests/test_taproot_annex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import unittest
import hashlib
import os
import binascii

from bitcoinutils.transactions import Transaction, TxInput, TxOutput
from bitcoinutils.script import Script
from bitcoinutils.constants import LEAF_VERSION_TAPSCRIPT, TAPROOT_SIGHASH_ALL
from bitcoinutils.utils import h_to_b, b_to_h


class TestSignatureHashAnnex(unittest.TestCase):
"""Test cases for signature hash annex functionality."""

def setUp(self):
# Create a simple transaction for testing
self.txin = TxInput(
"0" * 64, # Dummy txid
0, # Dummy index
)

self.txout = TxOutput(
10000, # 0.0001 BTC in satoshis
Script(["OP_1"]) # Dummy script
)

self.tx = Transaction(
[self.txin],
[self.txout],
has_segwit=True
)

# Create some dummy scripts and amounts for the tests
self.script_pubkeys = [Script(["OP_1"])]
self.amounts = [10000]

def test_taproot_digest_with_annex(self):
"""Test that adding an annex changes the signature hash."""

# Get digest without annex
digest_without_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL
)

# Get digest with annex
test_annex = h_to_b("aabbccdd") # Simple test annex
digest_with_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL,
annex=test_annex
)

# The digests should be different when an annex is provided
self.assertNotEqual(
digest_without_annex,
digest_with_annex,
"Signature hash should change when annex is provided"
)

def test_taproot_digest_different_annexes(self):
"""Test that different annexes produce different digests."""

# Get digest with first annex
first_annex = h_to_b("aabbccdd")
digest_with_first_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL,
annex=first_annex
)

# Get digest with second annex
second_annex = h_to_b("11223344")
digest_with_second_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL,
annex=second_annex
)

# Different annexes should produce different digests
self.assertNotEqual(
digest_with_first_annex,
digest_with_second_annex,
"Different annexes should produce different digests"
)

def test_taproot_digest_script_path_with_annex(self):
"""Test annex support with script path spending."""

# Get digest with script path without annex
digest_without_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
ext_flag=1, # Script path
script=Script(["OP_TRUE"]),
leaf_ver=LEAF_VERSION_TAPSCRIPT,
sighash=TAPROOT_SIGHASH_ALL
)

# Get digest with script path with annex
test_annex = h_to_b("ffee")
digest_with_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
ext_flag=1, # Script path
script=Script(["OP_TRUE"]),
leaf_ver=LEAF_VERSION_TAPSCRIPT,
sighash=TAPROOT_SIGHASH_ALL,
annex=test_annex
)

# The digests should be different
self.assertNotEqual(
digest_without_annex,
digest_with_annex,
"Signature hash should change when annex is provided in script path"
)

def test_empty_annex(self):
"""Test that an empty annex is handled properly."""

# Get digest without annex
digest_without_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL
)

# Get digest with empty annex
empty_annex = b""
digest_with_empty_annex = self.tx.get_transaction_taproot_digest(
txin_index=0,
script_pubkeys=self.script_pubkeys,
amounts=self.amounts,
sighash=TAPROOT_SIGHASH_ALL,
annex=empty_annex
)

# Even an empty annex should change the digest
self.assertNotEqual(
digest_without_annex,
digest_with_empty_annex,
"Signature hash should change even with empty annex"
)


if __name__ == "__main__":
unittest.main()