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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- (Notify of any improvements related to security vulnerabilities or potential risks.)

---
## [1.0.3] - 2025-03-26

### Fixed
Previously, the default fee rate was hardcoded to 10 satoshis per kilobyte. This update allows users to configure the default fee rate via the TRANSACTION_FEE_RATE variable in constants.py or through the environment file.

### Added
A test for the default fee rate has also been added.

### Changed
Optimized transaction preimage calculation by refactoring the tx_preimage function to directly compute the preimage for a specified input, avoiding unnecessary computation for all inputs
Achieved a 3× performance improvement in scenarios with 250 inputs, based on benchmarking


## [1.0.2] - 2025-02-28

### Added
Expand Down
2 changes: 1 addition & 1 deletion bsv/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
TRANSACTION_SEQUENCE: int = int(os.getenv('BSV_PY_SDK_TRANSACTION_SEQUENCE') or 0xffffffff)
TRANSACTION_VERSION: int = int(os.getenv('BSV_PY_SDK_TRANSACTION_VERSION') or 1)
TRANSACTION_LOCKTIME: int = int(os.getenv('BSV_PY_SDK_TRANSACTION_LOCKTIME') or 0)
TRANSACTION_FEE_RATE: float = float(os.getenv('BSV_PY_SDK_TRANSACTION_FEE_RATE') or 0.5) # satoshi per byte
TRANSACTION_FEE_RATE: int = int(os.getenv('BSV_PY_SDK_TRANSACTION_FEE_RATE') or 1) # satoshi per kilobyte
BIP32_DERIVATION_PATH = os.getenv('BSV_PY_SDK_BIP32_DERIVATION_PATH') or "m/"
BIP39_ENTROPY_BIT_LENGTH: int = int(os.getenv('BSV_PY_SDK_BIP39_ENTROPY_BIT_LENGTH') or 128)
BIP44_DERIVATION_PATH = os.getenv('BSV_PY_SDK_BIP44_DERIVATION_PATH') or "m/44'/236'/0'"
Expand Down
6 changes: 4 additions & 2 deletions bsv/hd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .bip32 import Xkey, Xprv, Xpub, ckd, step_to_index, master_xprv_from_seed
from .bip32 import Xkey, Xprv, Xpub, ckd, step_to_index, master_xprv_from_seed, bip32_derive_xprv_from_mnemonic, \
bip32_derive_xprvs_from_mnemonic, bip32_derive_xkeys_from_xkey
from .bip39 import WordList, mnemonic_from_entropy, seed_from_mnemonic, validate_mnemonic
from .bip44 import derive_xkeys_from_xkey, derive_xprvs_from_mnemonic, derive_xprv_from_mnemonic
from .bip44 import derive_xkeys_from_xkey, derive_xprvs_from_mnemonic, derive_xprv_from_mnemonic, \
bip44_derive_xprv_from_mnemonic, bip44_derive_xprvs_from_mnemonic
25 changes: 12 additions & 13 deletions bsv/hd/bip32.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from ..keys import PublicKey, PrivateKey



class Xkey:
"""
[ : 4] prefix
Expand Down Expand Up @@ -210,9 +209,9 @@ def master_xprv_from_seed(seed: Union[str, bytes], network: Network = Network.MA


def _derive_xkeys_from_xkey(xkey: Union[Xprv, Xpub],
index_start: Union[str, int],
index_end: Union[str, int],
change: Union[str, int] = 0) -> List[Union[Xprv, Xpub]]:
index_start: Union[str, int],
index_end: Union[str, int],
change: Union[str, int] = 0) -> List[Union[Xprv, Xpub]]:
"""
this function is internal use only within bip32 module
Use bip32_derive_xkeys_from_xkey instead.
Expand All @@ -236,14 +235,14 @@ def bip32_derive_xprv_from_mnemonic(mnemonic: str,


def bip32_derive_xprvs_from_mnemonic(mnemonic: str,
index_start: Union[str, int],
index_end: Union[str, int],
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP32_DERIVATION_PATH,
change: Union[str, int] = 0,
network: Network = Network.MAINNET) -> List[Xprv]:
index_start: Union[str, int],
index_end: Union[str, int],
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP32_DERIVATION_PATH,
change: Union[str, int] = 0,
network: Network = Network.MAINNET) -> List[Xprv]:
"""
Derive a range of extended keys from a nmemonic using BIP32 format
"""
Expand Down Expand Up @@ -295,4 +294,4 @@ def bip32_derive_xkeys_from_xkey(xkey: Union[Xprv, Xpub],
child_key = change_level.ckd(i)
derived_keys.append(child_key)

return derived_keys
return derived_keys
4 changes: 2 additions & 2 deletions bsv/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ def fee(self, model_or_fee=None, change_distribution='equal'):
"""

if model_or_fee is None:
model_or_fee = SatoshisPerKilobyte(10)
model_or_fee = SatoshisPerKilobyte(int(TRANSACTION_FEE_RATE))

if isinstance(model_or_fee, int):
fee = model_or_fee
else:
Expand Down
40 changes: 39 additions & 1 deletion bsv/transaction_preimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,42 @@ def tx_preimage(
tx_version: int,
tx_locktime: int,
) -> bytes:
return tx_preimages(inputs, outputs, tx_version, tx_locktime)[input_index]
"""
Calculates and returns the preimage for a specific input index.
"""
sighash = inputs[input_index].sighash

# hash previous outs
if not sighash & SIGHASH.ANYONECANPAY:
hash_prevouts = hash256(
b"".join(
bytes.fromhex(_in.source_txid)[::-1] + _in.source_output_index.to_bytes(4, "little")
for _in in inputs
)
)
else:
hash_prevouts = b"\x00" * 32

# hash sequence
if (
not sighash & SIGHASH.ANYONECANPAY
and sighash & 0x1F != SIGHASH.SINGLE
and sighash & 0x1F != SIGHASH.NONE
):
hash_sequence = hash256(
b"".join(_in.sequence.to_bytes(4, "little") for _in in inputs)
)
else:
hash_sequence = b"\x00" * 32

# hash outputs
if sighash & 0x1F != SIGHASH.SINGLE and sighash & 0x1F != SIGHASH.NONE:
hash_outputs = hash256(
b"".join(tx_output.serialize() for tx_output in outputs)
)
elif sighash & 0x1F == SIGHASH.SINGLE and input_index < len(outputs):
hash_outputs = hash256(outputs[input_index].serialize())
else:
hash_outputs = b"\x00" * 32

return _preimage(inputs[input_index], tx_version, tx_locktime, hash_prevouts, hash_sequence, hash_outputs)
74 changes: 49 additions & 25 deletions examples/hd.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,65 @@
from typing import List
from bsv.hd import mnemonic_from_entropy, seed_from_mnemonic, master_xprv_from_seed
from bsv.hd import bip32_derive_xprvs_from_mnemonic, bip44_derive_xprvs_from_mnemonic, bip32_derive_xkeys_from_xkey
from bsv.constants import BIP32_DERIVATION_PATH, BIP44_DERIVATION_PATH

from bsv.hd import mnemonic_from_entropy, seed_from_mnemonic, master_xprv_from_seed, Xprv, derive_xprvs_from_mnemonic
# You can set custom derivation paths in your environment variables as well
# BIP32_DERIVATION_PATH = "m/"
# BIP44_DERIVATION_PATH = "m/44'/236'/0'"

#
# HD derivation
# HD derivation (mnemonic, master-xpublickey, master-xprivatekey)
#
entropy = 'cd9b819d9c62f0027116c1849e7d497f'

# snow swing guess decide congress abuse session subway loyal view false zebra
# Generate mnemonic from entropy
mnemonic: str = mnemonic_from_entropy(entropy)
print(mnemonic)
print("Mnemonic:", mnemonic)

seed: bytes = seed_from_mnemonic(mnemonic)
print(seed.hex())

master_xprv: Xprv = master_xprv_from_seed(seed)
print(master_xprv)
# Generate seed from mnemonic
seed = seed_from_mnemonic(mnemonic, lang='en')
print("Seed:", seed.hex())

# Generate master keys
master_xprv = master_xprv_from_seed(seed)
master_xpub = master_xprv.xpub()
print("Master xprv:", master_xprv)
print("Master xpub:", master_xpub)
print()
keys: List[Xprv] = derive_xprvs_from_mnemonic(mnemonic, path="m/44'/0'/0'", change=1, index_start=0, index_end=5)
for key in keys:
# XPriv to WIF
print(key.private_key().wif())

key_xpub = key.xpub()
# Derive keys from mnemonic using BIP32
keys_from_mnemonic_by_bip32 = bip32_derive_xprvs_from_mnemonic(
mnemonic, 0, 3, path=BIP32_DERIVATION_PATH, change=0
)

# XPub to public key
print(key_xpub.public_key().hex())
print("Keys from mnemonic by BIP32:")
print("Address 0:", keys_from_mnemonic_by_bip32[0].address())
print("Private key 1:", keys_from_mnemonic_by_bip32[1].private_key().wif())
print("Public key 2:", keys_from_mnemonic_by_bip32[2].public_key().hex())
print()

# XPub to address
print(key_xpub.public_key().address(), '\n')
# Derive keys from xpub using BIP32
keys_from_xpub_by_bip32 = bip32_derive_xkeys_from_xkey(
master_xpub, 0, 3, change=0
)

print("Keys from xpub by BIP32:")
print("Address 0:", keys_from_xpub_by_bip32[0].address())
print("Public key 2:", keys_from_xpub_by_bip32[2].public_key().hex())
print()

#
# random mnemonic
#
# Derive keys from mnemonic using BIP44
bip44_keys = bip44_derive_xprvs_from_mnemonic(
mnemonic, 0, 3, path=BIP44_DERIVATION_PATH, change=0
)

print("Keys from mnemonic by BIP44:")
print("Address 0:", bip44_keys[0].address())
print("Private key 1:", bip44_keys[1].private_key().wif())
print("Public key 2:", bip44_keys[2].public_key().hex())
print()
print(mnemonic_from_entropy())
print(mnemonic_from_entropy(lang='en'))
print(mnemonic_from_entropy(lang='zh-cn'))

# Loop through multiple derived keys
print("All BIP44 derived keys:")
for i, key in enumerate(bip44_keys):
print(f"Address {i}: {key.address()}")
print(f"Private key {i}: {key.private_key().wif()}")
33 changes: 33 additions & 0 deletions tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,4 +667,37 @@ def test_input_auto_txid():
)


def test_transaction_fee_with_default_rate():
from bsv.constants import TRANSACTION_FEE_RATE

address = "1AfxgwYJrBgriZDLryfyKuSdBsi59jeBX9"
t = Transaction()
t_in = TransactionInput(
source_transaction=Transaction(
[],
[
None,
TransactionOutput(locking_script=P2PKH().lock(address), satoshis=1000),
],
),
source_txid="d2bc57099dd434a5adb51f7de38cc9b8565fb208090d9b5ea7a6b4778e1fdd48",
source_output_index=1,
unlocking_script_template=P2PKH().unlock(PrivateKey()),
)
t.add_input(t_in)
t.add_output(
TransactionOutput(
P2PKH().lock("1JDZRGf5fPjGTpqLNwjHFFZnagcZbwDsxw"), satoshis=100
)
)
t.add_output(TransactionOutput(P2PKH().lock(address), change=True))

t.fee()

estimated_size = t.estimated_byte_length()
expected_fee = int((estimated_size / 1000) * TRANSACTION_FEE_RATE)
actual_fee = t.get_fee()

assert abs(actual_fee - expected_fee) <= 1

# TODO: Test tx.verify()