Skip to content

Commit

Permalink
Add new p2pkh validation
Browse files Browse the repository at this point in the history
This commit adds a new implementation for the p2pkh verification, which
is more readable and should be similar to other scriptpubkey_type types.
  • Loading branch information
davidchocholaty committed Sep 5, 2024
1 parent 623a3cc commit 4109e53
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 112 deletions.
4 changes: 3 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def parse_arguments():

mempool = MemPool(args.mempool)

# TODO pokracovani

block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions

transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in block_transactions[1:]]
Expand All @@ -45,4 +47,4 @@ def parse_arguments():
print(block_hash)
print(coinbase_serialized.hex())
for transaction in transaction_hashes:
print(transaction)
print(transaction)
221 changes: 199 additions & 22 deletions src/transaction.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import hashlib
import json

from ecdsa import VerifyingKey, SECP256k1, BadSignatureError

from src.serialize import serialize_transaction
from src.utils import get_filename_without_extension
from src.verify import non_empty_vin_vout, valid_transaction_syntax, verify_p2pkh_transaction
from src.utils import decode_hex, get_filename_without_extension, hash160
from src.verify import valid_transaction_syntax

def calculate_txid(transaction_content, coinbase=False):
# Serialize the transaction content
Expand Down Expand Up @@ -35,41 +37,216 @@ def __init__(self, transaction_json_file):
self.vout = json_transaction['vout']
self.json_transaction = json_transaction
else:
# TODO jestli nejakej error
print('Invalid transaction syntax')

def is_valid(self):
if not non_empty_vin_vout(self.vin, self.vout):
# At least one input and one output.
if not self.non_empty_vin_vout():
return False

# Basic locktime check.
if not self.valid_locktime():
return False

if not self.check_input_output_sum():
return False

# Check each input validity.
for vin_idx, vin in enumerate(self.vin):
if not self.valid_input(vin_idx, vin):
return False

# Check each output validity.
for vout in self.vout:
if not self.valid_output(vout):
return False

return True

def non_empty_vin_vout(self):
# Make sure neither in or out lists are empty
if not self.vin:
#print("vin is empty")
return False
if not self.vout:
#print("vout is empty")
return False

return True

def valid_locktime(self):
try:
locktime_int = int(self.locktime)
except ValueError:
return False

if len(locktime_int) != 1 or self.locktime_int < 0:
return False

return True

def check_input_output_sum(self):
input_sum = 0
for input in self.vin:
input_sum = input_sum + input['prevout']['value']

output_sum = 0
for output in self.vout:
output_sum = output_sum + output['value']


# Output sum can't be greater than the input sum.
if input_sum < output_sum:
return False

input_idx = 0
for input in self.vin:
if 'scriptsig' in input:
scriptsig = input['scriptsig']

scriptpubkey_type = input['prevout']['scriptpubkey_type']

if scriptsig == "" or scriptpubkey_type not in ["p2pkh", "p2sh"]:
return False

if scriptpubkey_type == 'p2pkh':
if not verify_p2pkh_transaction(input_idx, self.json_transaction):
return False
else:
return False
else:
def valid_input(self, vin_idx, vin):
# TODO
if vin.get("is_coinbase", False):
return False

prevout = vin.get("prevout", {})
scriptpubkey_type = prevout.get("scriptpubkey_type", "")

if scriptpubkey_type == "p2pkh":
return self.validate_p2pkh(vin_idx, vin)
elif scriptpubkey_type == "p2sh":
pass
#return self.validate_p2sh(vin)
elif scriptpubkey_type == "v0_p2wsh":
pass
#return self.validate_p2wsh(vin)
elif scriptpubkey_type == "v1_p2tr":
pass
#return self.validate_p2tr(vin)
elif scriptpubkey_type == "v0_p2wpkh":
pass
#return self.validate_p2wpkh(vin)

# Unknown script type.
return False

def valid_output(self, vout):
scriptpubkey_type = vout.get("scriptpubkey_type", "")
return scriptpubkey_type in ["v0_p2wpkh", "p2sh", "v0_p2wsh", "v1_p2tr", "p2pkh"]

def validate_p2pkh(self, vin_idx, vin):
# Checking input signatures.
if "scriptsig" in vin:
prevout = vin.get("prevout", {})
scriptpubkey_type = prevout.get("scriptpubkey_type", "")

if vin["scriptsig"] == "":
return False

input_idx += 1
#if scriptpubkey_type == 'p2pkh':
# if not verify_p2pkh_transaction(input_idx, self.json_transaction):
# return False
#else:
# return False

#################
# Pubkey script #
#################

input_tx = vin[vin_idx]

scriptsig = decode_hex(input_tx.get("scriptsig", ""))

prevout = input_tx.get("prevout", "")

if prevout == "":
return False

scriptpubkey = decode_hex(prevout.get("scriptpubkey", ""))

###################
# Parse scriptSig #
###################
# https://learnmeabitcoin.com/technical/script/p2pkh/
# Explanation: the scriptSig contains the signature and the public key (including ASM instructions).
signature_len = scriptsig[0]
signature = scriptsig[1:1+signature_len]

public_key_idx = 1 + signature_len
public_key_len = scriptsig[public_key_idx]
public_key = scriptsig[public_key_idx+1:public_key_idx+1+public_key_len]

######################
# Parse scriptPubKey #
######################
# https://learnmeabitcoin.com/technical/script/p2pkh/
# Explanation: the scriptPubKey contains: DUP, HASH160, public key hash (including OP_PUSHBYTES_20), EQUALVERIFY and CHECKSIG.

if scriptpubkey[0:1] != b'\x76' or scriptpubkey[1:2] != b'\xa9' or scriptpubkey[2:3] != b'\x14':
return False # Not a valid P2PKH scriptPubKey (missing OP_DUP, OP_HASH160, or length mismatch)

if scriptpubkey[23:24] != b'\x88' or scriptpubkey[24:25] != b'\xac':
return False # Not a valid P2PKH scriptPubKey (missing OP_EQUALVERIFY or OP_CHECKSIG)

pkh = scriptpubkey[3:23]

# Compute the public key hash (HASH160 of the public key) and compare with scriptPubKey
calc_pkh = hash160(public_key)
if calc_pkh != pkh:
return False # Public key hash does not match

## --------------------------------------

"""# Extract data from input transaction
script_sig_asm = input_tx["scriptsig_asm"]
# Parse scriptSig ASM to extract signature and public key
script_parts = script_sig_asm.split(" ")
signature_hex = script_parts[1]
public_key_hex = script_parts[3]
r, s, hash_type = parse_der_signature(signature_hex)
r_hex = hex(r)[2:]
s_hex = hex(s)[2:]
der_len = len(signature_hex[:-2])
signature_len = len(r_hex + s_hex) + 2 * 6
if der_len != signature_len:
return False
signature = bytes.fromhex(r_hex + s_hex)
public_key = bytes.fromhex(public_key_hex)
scriptpubkey = bytes.fromhex(input_tx['prevout']['scriptpubkey'])
pubkey_hash = scriptpubkey[3:23]
hashed_public_key = hashlib.sha256(public_key).digest()
ripemd160 = RIPEMD160.new()
ripemd160.update(hashed_public_key)
pubkey_hash_calculated = ripemd160.digest()
if pubkey_hash != pubkey_hash_calculated:
return False
"""

############################################
# Verify the signature with the public key #
############################################

# Remove the SIGHASH type from the signature.
hash_type = signature[-1]
signature = signature[:-1]

data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type))
data_hash = hashlib.sha256(data_signed).digest()

# Verify the signature
verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1)
try:
verifying_key.verify(signature, data_hash, hashlib.sha256)
except BadSignatureError:
return False

return True


return True
return False
12 changes: 11 additions & 1 deletion src/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import hashlib
import os

def get_filename_without_extension(file_path):
# Get the base filename from the path
filename = os.path.basename(file_path)
# Remove the extension
filename_without_extension = os.path.splitext(filename)[0]
return filename_without_extension
return filename_without_extension

def decode_hex(hex_data):
# Decode a hex-encoded data into its raw bytecode.
return bytes.fromhex(hex_data)

def hash160(data):
# SHA-256 followed by RIPEMD-160 (Bitcoin's HASH160).
sha256_hash = hashlib.sha256(data).digest()
return hashlib.new('ripemd160', sha256_hash).digest()
Loading

0 comments on commit 4109e53

Please sign in to comment.