Skip to content
Merged
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
105 changes: 105 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
.idea/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
20 changes: 15 additions & 5 deletions bitcoinutils/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ def sign_message(self, message, compressed=True):


def sign_input(self, tx, txin_index, script, sighash=SIGHASH_ALL):
# the tx knows how to calculate the digest for the corresponding
# sighash)
tx_digest = tx.get_transaction_digest(txin_index, script, sighash)
return self._sign_input(tx_digest, sighash)


def sign_segwit_input(self, tx, txin_index, script, amount, sighash=SIGHASH_ALL):
# the tx knows how to calculate the digest for the corresponding
# sighash)
tx_digest = tx.get_transaction_segwit_digest(txin_index, script, amount, sighash)
return self._sign_input(tx_digest, sighash)


def _sign_input(self, tx_digest, sighash=SIGHASH_ALL):
"""Signs a transaction input with the private key

Bitcoin uses the normal DER format for transactions. Each input is
Expand All @@ -256,10 +270,6 @@ def sign_input(self, tx, txin_index, script, sighash=SIGHASH_ALL):
Returns a signature for that input
"""

# the tx knows how to calculate the digest for the corresponding
# sighash)
tx_digest = tx.get_transaction_digest(txin_index, script, sighash)

# note that deterministic signing is used
signature = self.key.sign_digest_deterministic(tx_digest,
sigencode=sigencode_der,
Expand Down Expand Up @@ -1065,4 +1075,4 @@ def main():
pass

if __name__ == "__main__":
main()
main()
18 changes: 15 additions & 3 deletions bitcoinutils/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# LICENSE file.

import struct
import copy
import hashlib
from binascii import unhexlify, hexlify

Expand Down Expand Up @@ -165,6 +166,11 @@ def __init__(self, script):

self.script = script

@classmethod
def copy(cls, script):
"""Deep copy of Script"""
scripts = copy.deepcopy(script.script)
return cls(scripts)

def _op_push_data(self, data):
"""Converts data to appropriate OP_PUSHDATA OP code including length
Expand All @@ -190,6 +196,9 @@ def _op_push_data(self, data):
else:
raise ValueError("Data too large. Cannot push into script")

def _segwit_op_push_data(self, data):
data_bytes = unhexlify(data)
return chr(len(data_bytes)).encode() + data_bytes

def _push_integer(self, integer):
"""Converts integer to bytes; as signed little-endian integer
Expand All @@ -214,7 +223,7 @@ def _push_integer(self, integer):
return self._op_push_data( hexlify(integer_bytes) )


def to_bytes(self):
def to_bytes(self, segwit = False):
"""Converts the script to bytes

If an OP code the appropriate byte is included according to:
Expand All @@ -235,7 +244,11 @@ def to_bytes(self):
if type(token) is int:
script_bytes += self._push_integer(token)
else:
script_bytes += self._op_push_data(token)
if segwit:
script_bytes += self._segwit_op_push_data(token)
else:
script_bytes += self._op_push_data(token)

return script_bytes

def to_hex(self):
Expand Down Expand Up @@ -265,4 +278,3 @@ def to_p2wsh_script_pub_key(self):
return Script(['OP_0', hexlify(sha256).decode('utf-8')])



129 changes: 121 additions & 8 deletions bitcoinutils/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ class TxInput:
creates a copy of the object (classmethod)
"""

def __init__(self, txid, txout_index, script_sig=Script([]),
sequence=DEFAULT_TX_SEQUENCE):
def __init__(self, txid, txout_index, script_sig=Script([]), sequence=DEFAULT_TX_SEQUENCE):
"""See TxInput description"""

# expected in the format used for displaying Bitcoin hashes
self.txid = txid
self.txout_index = txout_index
self.script_sig = script_sig

# if user provided a sequence it would be as string (for now...)
if type(sequence) is str:
self.sequence = unhexlify(sequence)
Expand Down Expand Up @@ -273,10 +273,12 @@ class Transaction:
"""

def __init__(self, inputs=[], outputs=[], locktime=DEFAULT_TX_LOCKTIME,
version=DEFAULT_TX_VERSION):
version=DEFAULT_TX_VERSION, has_segwit = False, witnesses = []):
"""See Transaction description"""
self.inputs = inputs
self.outputs = outputs
self.has_segwit = has_segwit
self.witnesses = witnesses

# if user provided a locktime it would be as string (for now...)
if type(locktime) is str:
Expand All @@ -293,7 +295,8 @@ def copy(cls, tx):

ins = [TxInput.copy(txin) for txin in tx.inputs]
outs = [TxOutput.copy(txout) for txout in tx.outputs]
return cls(ins, outs, tx.locktime, tx.version)
witnes = [Script.copy(witne) for witne in tx.witnesses]
return cls(ins, outs, tx.locktime, tx.version, tx.has_segwit, witnes)


def get_transaction_digest(self, txin_index, script, sighash=SIGHASH_ALL):
Expand Down Expand Up @@ -381,7 +384,7 @@ def get_transaction_digest(self, txin_index, script, sighash=SIGHASH_ALL):
tmp_tx.inputs = [tmp_tx.inputs[txin_index]]

# get the byte stream of the temporary transaction
tx_for_signing = tmp_tx.stream()
tx_for_signing = tmp_tx.stream(False)

# add sighash bytes to be hashed
# Note that although sighash is one byte it is hashed as a 4 byte value.
Expand All @@ -397,10 +400,114 @@ def get_transaction_digest(self, txin_index, script, sighash=SIGHASH_ALL):
return tx_digest


def stream(self):
def get_transaction_segwit_digest(self, txin_index, script, amount, sighash=SIGHASH_ALL):
"""Returns the segwit transaction's digest for signing.

| SIGHASH types (see constants.py):
| SIGHASH_ALL - signs all inputs and outputs (default)
| SIGHASH_NONE - signs all of the inputs
| SIGHASH_SINGLE - signs all inputs but only txin_index output
| SIGHASH_ANYONECANPAY (only combined with one of the above)
| - with ALL - signs all outputs but only txin_index input
| - with NONE - signs only the txin_index input
| - with SINGLE - signs txin_index input and output

Attributes
----------
txin_index : int
The index of the input that we wish to sign
script : list (string)
The scriptPubKey of the UTXO that we want to spend
sighash : int
The type of the signature hash to be created
"""

# clone transaction to modify without messing up the real transaction
tmp_tx = Transaction.copy(self)

bos_hash_prevouts = b'\x00' * 32
hash_sequence = b'\x00' * 32
hash_outputs = b'\x00' * 32

# Judging the signature type
basic_sig_hash_type = (sighash & 0x1f)
anyone_can_pay = sighash& 0xf0 == SIGHASH_ANYONECANPAY
sign_all = (basic_sig_hash_type != SIGHASH_SINGLE) and (basic_sig_hash_type != SIGHASH_NONE)

# Hash all input
if not anyone_can_pay:
bos_hash_prevouts = b''
for txin in tmp_tx.inputs:
bos_hash_prevouts += unhexlify(txin.txid)[::-1] + \
struct.pack('<L', txin.txout_index)
bos_hash_prevouts = hashlib.sha256(hashlib.sha256(bos_hash_prevouts).digest()).digest()

# Hash all input sequence
if not anyone_can_pay and sign_all:
hash_sequence = b''
for txin in tmp_tx.inputs:
hash_sequence += txin.sequence
hash_sequence = hashlib.sha256(hashlib.sha256(hash_sequence).digest()).digest()

if sign_all:
# Hash all output
hash_outputs = b''
for txout in tmp_tx.outputs:
amount_bytes = struct.pack('<q', round(txout.amount * SATOSHIS_PER_BITCOIN))
script_bytes = txout.script_pubkey.to_bytes()
hash_outputs += amount_bytes + struct.pack('B', len(script_bytes)) + script_bytes
hash_outputs = hashlib.sha256(hashlib.sha256(hash_outputs).digest()).digest()
elif basic_sig_hash_type == SIGHASH_SINGLE and txin_index < len(tmp_tx.outputs):
# Hash one output
txout = tmp_tx.outputs[txin_index]
amount_bytes = struct.pack('<q', round(txout.amount * SATOSHIS_PER_BITCOIN))
script_bytes = txout.script_pubkey.to_bytes()
hash_outputs = amount_bytes + struct.pack('B', len(script_bytes)) + script_bytes
hash_outputs = hashlib.sha256(hashlib.sha256(hash_outputs).digest()).digest()

# add sighash bytes to be hashed
tx_for_signing = self.version

# add sighash bytes to be hashed
tx_for_signing += bos_hash_prevouts + hash_sequence

# add tx txin
txin = self.inputs[txin_index]
tx_for_signing += unhexlify(txin.txid)[::-1] + \
struct.pack('<L', txin.txout_index)

# add tx sign
tx_for_signing += struct.pack('B', len(script.to_bytes()))
tx_for_signing += script.to_bytes()

# add txin amount
tx_for_signing += struct.pack('<q', round(amount * SATOSHIS_PER_BITCOIN))

# add tx sequence
tx_for_signing += txin.sequence

# add txouts hash
tx_for_signing += hash_outputs

# add locktime
tx_for_signing += self.locktime

# add sighash type
tx_for_signing += struct.pack('<i', sighash)

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


def stream(self,has_segwit):
"""Converts to bytes"""

data = self.version
if has_segwit:
# marker
data += b'\x00'
# flag
data += b'\x01'

txin_count_bytes = chr(len(self.inputs)).encode()
txout_count_bytes = chr(len(self.outputs)).encode()
data += txin_count_bytes
Expand All @@ -409,14 +516,20 @@ def stream(self):
data += txout_count_bytes
for txout in self.outputs:
data += txout.stream()
if has_segwit:
for witnesse in self.witnesses:
# add witnesses script Count
witnesses_count_bytes = chr(len(witnesse.script)).encode()
data += witnesses_count_bytes
data += witnesse.to_bytes(True)
data += self.locktime
return data


def get_txid(self):
"""Hashes the serialized tx to get a unique id"""

data = self.stream()
data = self.stream(self.has_segwit)
hash = hashlib.sha256( hashlib.sha256(data).digest() ).digest()
# note that we reverse the hash for display purposes
return hexlify(hash[::-1]).decode('utf-8')
Expand All @@ -425,7 +538,7 @@ def get_txid(self):
def serialize(self):
"""Converts to hex string"""

return hexlify(self.stream()).decode('utf-8')
return hexlify(self.stream(self.has_segwit)).decode('utf-8')


def main():
Expand Down
Loading