Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

202308 #334

Merged
merged 8 commits into from
Sep 11, 2023
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
12 changes: 6 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
Python Bitcoin Library
======================

Bitcoin Crypto Currency Library for Python.
Bitcoin cryptocurrency Library writen in Python.

Includes a fully functional wallet, with multi signature, multi currency and multiple accounts. Use this
library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases and blocks with
Allows you to create a fully functional Bitcoin wallet with a single line of code.
Use this library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases and blocks with
simple and straightforward Python code.

You this library at a high level and create and manage wallets for the command line or at a low level
and create your own custom made transactions, keys or wallets.
You can use this library at a high level and create and manage wallets from the command line or at a low level
and create your own custom made transactions, scripts, keys or wallets.

The BitcoinLib connects to various service providers automatically to update wallets, transactions and
The BitcoinLib connects to various service providers automatically to update wallets, transaction and
blockchain information.

.. image:: https://github.com/1200wd/bitcoinlib/actions/workflows/unittests.yaml/badge.svg
Expand Down
74 changes: 49 additions & 25 deletions bitcoinlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'sig_pubkey': ('unlocking', ['signature', 'key'], []),
# 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []),
'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []),
'p2tr_unlock': ('unlocking', ['data'], [64]),
'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []),
'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []),
'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []),
Expand Down Expand Up @@ -155,7 +156,7 @@ def get_data_type(data):
elif ((data.startswith(b'\x02') or data.startswith(b'\x03')) and len(data) == 33) or \
(data.startswith(b'\x04') and len(data) == 65):
return 'key'
elif len(data) == 20 or len(data) == 32 or 1 < len(data) <= 4:
elif len(data) == 20 or len(data) == 32 or len(data) == 64 or 1 < len(data) <= 4:
return 'data-%d' % len(data)
else:
return 'other'
Expand Down Expand Up @@ -282,14 +283,17 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0):

:return Script:
"""
data_length = None
if isinstance(script, bytes):
data_length = len(script) // 2
script = BytesIO(script)
elif isinstance(script, str):
data_length = len(script)
script = BytesIO(bytes.fromhex(script))
return cls.parse_bytesio(script, message, tx_data, strict, _level)
return cls.parse_bytesio(script, message, tx_data, data_length, strict, _level)

@classmethod
def parse_bytesio(cls, script, message=None, tx_data=None, strict=True, _level=0):
def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict=True, _level=0):
"""
Parse raw script and return Script object. Extracts script commands, keys, signatures and other data.

Expand All @@ -299,6 +303,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, strict=True, _level=0
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
:param data_length: Length of script data if known. Supply if you can to increase efficiency and lower change of incorrect parsing
:type data_length: int
:param strict: Raise exception when script is malformed, incomplete or not understood. Default is True
:type strict: bool
:param _level: Internal argument used to avoid recursive depth
Expand All @@ -317,28 +323,20 @@ def parse_bytesio(cls, script, message=None, tx_data=None, strict=True, _level=0
if not tx_data:
tx_data = {}

while script:
chb = script.read(1)
if not chb:
break
ch = int.from_bytes(chb, 'big')
data = None
chb = script.read(1)
ch = int.from_bytes(chb, 'big')
data = None
if chb == b'\x30' and 69 <= data_length <= 74:
data = chb + script.read(data_length - 1)
elif ((chb == b'\x02' or chb == b'\x03') and data_length == 33) or \
(chb == b'\x04' and data_length == 65):
data = chb + script.read(data_length - 1)
elif data_length == 64:
data = chb + script.read(data_length - 1)
else:
data_length = 0
if 1 <= ch <= 75: # Data`
data_length = ch
elif ch == op.op_pushdata1:
data_length = int.from_bytes(script.read(1), 'little')
elif ch == op.op_pushdata2:
data_length = int.from_bytes(script.read(2), 'little')
if data_length:
data = script.read(data_length)
if len(data) != data_length:
msg = "Malformed script, not enough data found"
if strict:
raise ScriptError(msg)
else:
_logger.warning(msg)

while chb and script:
if data:
data_type = get_data_type(data)
commands.append(data)
Expand Down Expand Up @@ -384,10 +382,32 @@ def parse_bytesio(cls, script, message=None, tx_data=None, strict=True, _level=0
sigs_required = s2.sigs_required
except (ScriptError, IndexError):
blueprint.append('data-%d' % len(data))
data = None
data_length = 0
else: # Other opcode
if 1 <= ch <= 75: # Data`
data_length = ch
elif ch == op.op_pushdata1:
data_length = int.from_bytes(script.read(1), 'little')
elif ch == op.op_pushdata2:
data_length = int.from_bytes(script.read(2), 'little')
if data_length:
data = script.read(data_length)
if len(data) != data_length:
msg = "Malformed script, not enough data found"
if strict:
raise ScriptError(msg)
else:
chb = b''
_logger.warning(msg)
continue

commands.append(ch)
blueprint.append(ch)

chb = script.read(1)
ch = int.from_bytes(chb, 'big')

s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, tx_data=tx_data,
hash_type=hash_type)
script.seek(0)
Expand All @@ -410,6 +430,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, strict=True, _level=0
(len(s.keys), s.commands[-2] - 80))
elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr'] and len(s.commands) > 1:
s.public_hash = s.commands[1]
elif st == 'p2tr_unlock':
s.public_hash = s.commands[0]
elif st == 'p2pkh' and len(s.commands) > 2:
s.public_hash = s.commands[2]
s.redeemscript = redeemscript if redeemscript else s.redeemscript
Expand Down Expand Up @@ -443,7 +465,8 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0):

:return Script:
"""
return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, tx_data, strict, _level)
data_length = len(script) // 2
return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, tx_data, data_length, strict, _level)

@classmethod
def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0):
Expand All @@ -465,7 +488,8 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0):

:return Script:
"""
return cls.parse_bytesio(BytesIO(script), message, tx_data, strict, _level)
data_length = len(script)
return cls.parse_bytesio(BytesIO(script), message, tx_data, data_length, strict, _level)

def __repr__(self):
s_items = []
Expand Down
2 changes: 1 addition & 1 deletion bitcoinlib/tools/wallet_multisig_2of3.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
key_lists[w[0]].append(addkey)

offline_wallet = Wallet.create(WALLET_NAME, key_lists['Offline PC'], sigs_required=SIGNATURES_REQUIRED,
witness_type=WITNESS_TYPE, network=NETWORK)
witness_type=WITNESS_TYPE, network=NETWORK)
offline_wallet.new_key()

print("\n\nA multisig wallet has been created on this system")
Expand Down
28 changes: 23 additions & 5 deletions bitcoinlib/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ def update_scripts(self, hash_type=SIGHASH_ALL):
if b'' in signatures:
raise TransactionError("Empty signature found in signature list when signing. "
"Is DER encoded version of signature defined?")
if len(signatures) == self.sigs_required: # and not self.unlocking_script
if len(signatures) and len(signatures) >= self.sigs_required: # and not self.unlocking_script
unlock_script_obj = Script(script_types=['p2sh_multisig'], keys=[k.public_byte for k in self.keys],
signatures=self.signatures[:self.sigs_required],
sigs_required=self.sigs_required, redeemscript=self.redeemscript)
Expand Down Expand Up @@ -984,6 +984,9 @@ def update_scripts(self, hash_type=SIGHASH_ALL):
addr_data = self.keys[0].public_byte
if self.signatures and not self.unlocking_script:
self.unlocking_script = varstr(self.signatures[0].as_der_encoded())
elif self.script_type == 'p2tr': # segwit_v1
self.redeemscript = self.witnesses[0]
# FIXME: Address cannot be known without looking at previous transaction
elif self.script_type not in ['coinbase', 'unknown'] and self.strict:
raise TransactionError("Unknown unlocking script type %s for input %d" % (self.script_type, self.index_n))
if addr_data and not self.address:
Expand Down Expand Up @@ -1475,25 +1478,40 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK):
if not n_items:
continue
script = Script()
is_taproot = False
for m in range(0, n_items):
item_size = read_varbyteint(rawtx)
witness = rawtx.read(item_size)
if item_size == 0:
witness = b'\0'
else:
witness = rawtx.read(item_size)
inputs[n].witnesses.append(witness)
s = Script.parse_bytes(varstr(witness), strict=strict)
script += s
if not is_taproot:
s = Script.parse_bytes(witness, strict=strict)
if s.script_types == ['p2tr_unlock']:
# FIXME: Support Taproot unlocking scripts
_logger.warning("Taproot is not supported at the moment, rest of parsing input transaction "
"skipped")
is_taproot = True
script += s

inputs[n].script = script if not inputs[n].script else inputs[n].script + script
inputs[n].keys = script.keys
inputs[n].signatures = script.signatures
if script.script_types[0][:13] == 'p2sh_multisig' or script.script_types[0] == 'signature_multisig': # , 'p2sh_p2wsh'
if script.script_types[0][:13] == 'p2sh_multisig' or script.script_types[0] =='signature_multisig':
inputs[n].script_type = 'p2sh_multisig'
inputs[n].redeemscript = inputs[n].witnesses[-1]
elif script.script_types[0] == 'p2tr_unlock':
inputs[n].script_type = 'p2tr'
inputs[n].witness_type = 'segwit'
elif inputs[n].script_type == 'p2wpkh':
inputs[n].script_type = 'p2sh_p2wpkh'
inputs[n].witness_type = 'p2sh-segwit'
elif inputs[n].script_type == 'p2wpkh' or inputs[n].script_type == 'p2wsh':
inputs[n].script_type = 'p2sh_p2wsh'
inputs[n].witness_type = 'p2sh-segwit'
elif 'unknown' in script.script_types and not coinbase:
inputs[n].script_type = 'unknown'

inputs[n].update_scripts()

Expand Down
10 changes: 5 additions & 5 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
Welcome to Bitcoinlib's documentation!
======================================

Bitcoin Crypto Currency Library for Python.
Bitcoin cryptocurrency Library writen in Python.

Includes a fully functional wallet, with multi signature, multi currency and multiple accounts. Use this
library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases and blocks with
Allows you to create a fully functional wallet with a single line of code.
Use this library to create and manage transactions, addresses/keys, wallets, mnemonic password phrases and blocks with
simple and straightforward Python code.

You can use this library at a high level and create and manage wallets on the command line or at a low level
You can use this library at a high level and create and manage wallets from the command line or at a low level
and create your own custom made transactions, scripts, keys or wallets.

The BitcoinLib connects to various service providers automatically to update wallets, transactions and
The BitcoinLib connects to various service providers automatically to update wallets, transaction and
blockchain information.


Expand Down
2 changes: 1 addition & 1 deletion examples/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
pk3 = HDKey()
t = Transaction()
test_txid = 'f3d9b08dbd873631aaca66a1d18342ba24a22437ea107805405f6bedd3851618'
t.add_input(prev_txid=test_txid, output_n=0,
t.add_input(prev_txid=test_txid, output_n=0, value=100000,
keys=[pk1.public_byte, pk2.public_byte, pk3.public_byte],
script_type='p2sh_multisig', sigs_required=2)
t.add_output(100000, '1HsZBGm6nNGG1Moc3TL6S9DSGbnPbsSyW3')
Expand Down
Loading