Bitcoin transaction signature validation problems
To validate each input of a Bitcoin transaction using ECDSA on the secp256k1 curve, we need three parameters:
- Public key (Uncompressed example:
042e930f39ba62c6534ee98ed20ca98959d34aa9e057cda01cfd422c6bab3667b76426529382c23f42b9b08d7832d4fee1d6b437a8526e59667ce9c4e9dcebcabb) - Signature (Considered in DER format:
30450221009908144ca6539e09512b9295c8a27050d478fbb96f8addbc3d075544dc41328702201aa528be2b907d316d2da068dd9eb1e23243d97e444d59290d2fddf25269ee0e, last byte 01 SIGHASH_ALL removed) - Double hash of raw transaction data that the sender originally signed (Found hash:
083867478cb0d1d8bb864175bbc49728cffcc114bc2e762c6df64f2c965a9a66)
Here’s the documentation on how to create transaction data for signing https://en.bitcoin.it/wiki/OP_CHECKSIG
Everywhere in documentation and books, it is stated how to assemble this data and then compute its double SHA-256 hash
I attempted to gather all the transaction data from this pizza transaction (https://blockstream.info/api/tx/cca7507897abc89628f450e8b1e0c6fca4ec3f7b34cccf55f3f531c659ff4d79) and followed the step-by-step provided in this Bitcoin StackExchange answer
- But the author got: 692678553d1b85ccf87d4d4443095f276cdf600f2bb7dd44f6effbd7458fd4c2 after double hashing
- And I tried hash it ones, got this: 083867478cb0d1d8bb864175bbc49728cffcc114bc2e762c6df64f2c965a9a66
Used online EDCDSA verification tool https://emn178.github.io/online-tools/ecdsa/verify/
Then look, my input was correct and originally signed message was like this 083867478cb0d1d8bb864175bbc49728cffcc114bc2e762c6df64f2c965a9a66
DOUBLE SHA256:

ONE SHA256:

I wrote a script that takes a transaction ID and validates its inputs, but my script currently returns an entirely incorrect input (public key and signature correct)
Questions:
Here is my code:
import requests
import hashlib
from bitcoin.core import CTransaction, CTxIn, CTxOut, COutPoint, x, b2x, lx
from bitcoin.core.script import CScript
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.exceptions import InvalidSignature
import logging
def fetch_raw_transaction(transaction_id):
"""
Fetch the raw transaction hex from Blockstream API.
:param transaction_id: Transaction ID (txid)
:return: Raw transaction hex
"""
url = f"https://blockstream.info/api/tx/{transaction_id}/hex"
response = requests.get(url)
if response.status_code == 200:
logging.debug(f"Raw transaction fetched: {response.text.strip()}")
return bytes.fromhex(response.text.strip()) # Return as bytes
else:
raise Exception(f"Failed to fetch transaction. Status code: {response.status_code}")
def fetch_utxo_script_pubkey(txid, vout):
"""
Fetch the ScriptPubKey for a given UTXO using Blockstream API.
:param txid: Transaction ID of the UTXO
:param vout: Output index of the UTXO
:return: ScriptPubKey as bytes
"""
url = f"https://blockstream.info/api/tx/{txid}"
response = requests.get(url)
if response.status_code == 200:
outputs = response.json().get('vout', [])
if 0 <= vout < len(outputs):
script_pubkey = outputs[vout].get('scriptpubkey', None)
if script_pubkey:
return bytes.fromhex(script_pubkey)
else:
raise Exception(f"ScriptPubKey not found for vout {vout} in transaction {txid}")
else:
raise Exception(f"Invalid vout index: {vout} for transaction {txid}")
else:
raise Exception(f"Failed to fetch transaction details. Status code: {response.status_code}")
def compute_sighash(transaction_id, input_index, script_pubkey):
"""
Compute the sighash for a specific input in a Bitcoin transaction.
:param transaction_id: Transaction ID (txid)
:param input_index: Index of the input being signed
:param script_pubkey: ScriptPubKey for the input
:return: SIGHASH (message for signature)
"""
try:
# Fetch the raw transaction bytes
raw_tx_bytes = fetch_raw_transaction(transaction_id)
logging.debug(f"Raw transaction bytes: {raw_tx_bytes.hex()}")
# Deserialize the transaction
tx = CTransaction.deserialize(raw_tx_bytes)
logging.debug(f"Deserialized transaction: {tx}")
# Create a new transaction with updated scriptSig
modified_tx_ins = []
for i, tx_in in enumerate(tx.vin):
if i == input_index:
# Replace scriptSig for the specified input
modified_tx_ins.append(CTxIn(tx_in.prevout, CScript(script_pubkey), tx_in.nSequence))
else:
# Keep other inputs unchanged
modified_tx_ins.append(tx_in)
modified_tx = CTransaction(modified_tx_ins, tx.vout, tx.nLockTime)
logging.debug(f"Modified transaction: {modified_tx}")
# Serialize transaction with SIGHASH_ALL (0x01)
tx_raw_with_sighash = modified_tx.serialize() + bytes([1]) # 0x01 = SIGHASH_ALL
logging.debug(f"Serialized transaction with SIGHASH_ALL: {tx_raw_with_sighash.hex()}")
# Compute double SHA-256
sighash = hashlib.sha256(hashlib.sha256(tx_raw_with_sighash).digest()).digest()
logging.debug(f"Computed SIGHASH: {sighash.hex()}")
return sighash
except Exception as e:
logging.error("Error during SIGHASH computation", exc_info=True)
raise
def verify_signature(public_key_hex, signature_hex, message):
"""
Verify the signature using ECDSA.
:param public_key_hex: Hex-encoded public key
:param signature_hex: Hex-encoded signature
:param message: Message (hash) to verify
:return: True if valid, raises exception otherwise
"""
try:
public_key_bytes = bytes.fromhex(public_key_hex)
signature_bytes = bytes.fromhex(signature_hex[:-2]) # Remove the last byte (SIGHASH flag)
# Load the public key
public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key_bytes)
# Verify the signature
public_key.verify(signature_bytes, message, ec.ECDSA(SHA256()))
print("Signature is valid.")
except InvalidSignature:
print("Signature is invalid.")
except Exception as e:
logging.error("Error during signature verification", exc_info=True)
raise
if __name__ == "__main__":
transaction_id = "cca7507897abc89628f450e8b1e0c6fca4ec3f7b34cccf55f3f531c659ff4d79"
try:
# Fetch the raw transaction bytes
raw_tx_bytes = fetch_raw_transaction(transaction_id)
tx = CTransaction.deserialize(raw_tx_bytes)
# Iterate over each input to validate signatures
for i, tx_in in enumerate(tx.vin):
print(f"\nValidating input {i}:")
# Extract public key and signature from scriptSig
script_sig = tx_in.scriptSig
signature_hex = b2x(script_sig[1:1 + script_sig[0]])
public_key_hex = b2x(script_sig[1 + script_sig[0] + 1:])
print(f"Public Key: {public_key_hex}")
print(f"Signature: {signature_hex}")
# Fetch ScriptPubKey for this input
prev_txid = b2x(tx_in.prevout.hash[::-1]) # Convert to little-endian
script_pubkey = fetch_utxo_script_pubkey(prev_txid, tx_in.prevout.n)
print(f"ScriptPubKey: {script_pubkey.hex()}")
# Compute sighash
sighash = compute_sighash(transaction_id, i, script_pubkey)
print(f"Computed SIGHASH (message): {sighash.hex()}")
# Verify signature
verify_signature(public_key_hex, signature_hex, sighash)
except Exception as e:
print("Error:", str(e))
Output:
Validating input 0:
Public Key: 042e930f39ba62c6534ee98ed20ca98959d34aa9e057cda01cfd422c6bab3667b76426529382c23f42b9b08d7832d4fee1d6b437a8526e59667ce9c4e9dcebcabb
Signature: 30450221009908144ca6539e09512b9295c8a27050d478fbb96f8addbc3d075544dc41328702201aa528be2b907d316d2da068dd9eb1e23243d97e444d59290d2fddf25269ee0e01
ScriptPubKey: 76a91446af3fb481837fadbb421727f9959c2d32a3682988ac
Computed SIGHASH (message): 48fd60a2aeb4247255d2c1929c28ab47bdaaa49b12ff17e50b742789a3604602
Signature is invalid.
Expected Computed SIGHASH (message) is 083867478cb0d1d8bb864175bbc49728cffcc114bc2e762c6df64f2c965a9a66
Before offering advice, please test the code and verify the correctness of the solution.
I have already reviewed many answers on how to correctly assemble the message for transaction signing, but none of them work
If there is any API that return signed hash of transaction, so I can validate it, please tell me












