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
1 change: 1 addition & 0 deletions bsv/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
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
102 changes: 99 additions & 3 deletions bsv/hd/bip32.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import hmac
from hashlib import sha512
from typing import Union
from typing import Union, List

from .bip39 import seed_from_mnemonic
from ..base58 import base58check_decode, base58check_encode
from ..constants import BIP32_SEED_BYTE_LENGTH
from ..constants import NETWORK_XPUB_PREFIX_DICT, NETWORK_XPRV_PREFIX_DICT
from ..constants import Network, XKEY_BYTE_LENGTH, XKEY_PREFIX_LIST, PUBLIC_KEY_COMPRESSED_PREFIX_LIST
from ..constants import XPUB_PREFIX_NETWORK_DICT, XPRV_PREFIX_NETWORK_DICT
from ..constants import XPUB_PREFIX_NETWORK_DICT, XPRV_PREFIX_NETWORK_DICT, BIP32_DERIVATION_PATH
from ..curve import curve, curve_add, curve_multiply
from ..keys import PublicKey, PrivateKey



class Xkey:
"""
[ : 4] prefix
Expand Down Expand Up @@ -65,7 +67,11 @@ def ckd(self, index: Union[int, str, bytes]) -> 'Xpub':
elif isinstance(index, str):
index = bytes.fromhex(index)
assert len(index) == 4, 'index should be a 4 bytes integer'
assert index[0] < 0x80, "can't make hardened derivation from xpub"
assert index[0] < 0x80, ("can't make hardened derivation from xpub. "
"If you use hardened key, please set xpub with path from xpriv first. Example:\n"
" master_xprv = master_xprv_from_seed(seed)\n"
" account_xprv = ckd(master_xprv, \"m/44'/0'/0'\")\n"
" account_xpub = account_xprv.xpub()")

payload: bytes = self.prefix
payload += (self.depth + 1).to_bytes(1, 'big')
Expand Down Expand Up @@ -181,6 +187,7 @@ def step_to_index(step: Union[str, int]) -> int:

def ckd(xkey: Union[Xprv, Xpub], path: str) -> Union[Xprv, Xpub]:
"""
ckd = "Child Key Derivation"
derive an extended key according to path like "m/44'/0'/1'/0/10" (absolute) or "./0/10" (relative)
"""
steps = path.strip(' ').strip('/').split('/')
Expand All @@ -200,3 +207,92 @@ def ckd(xkey: Union[Xprv, Xpub], path: str) -> Union[Xprv, Xpub]:

def master_xprv_from_seed(seed: Union[str, bytes], network: Network = Network.MAINNET) -> Xprv:
return Xprv.from_seed(seed, network)


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]]:
"""
this function is internal use only within bip32 module
Use bip32_derive_xkeys_from_xkey instead.
"""
change_xkey = xkey.ckd(step_to_index(change))
return [change_xkey.ckd(i) for i in range(step_to_index(index_start), step_to_index(index_end))]


def bip32_derive_xprv_from_mnemonic(mnemonic: str,
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP32_DERIVATION_PATH,
network: Network = Network.MAINNET) -> Xprv:
"""
Derive the subtree root extended private key from mnemonic and path.
"""
seed = seed_from_mnemonic(mnemonic, lang, passphrase, prefix)
master_xprv = Xprv.from_seed(seed, network)
return ckd(master_xprv, path)


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]:
"""
Derive a range of extended keys from a nmemonic using BIP32 format
"""
xprv = bip32_derive_xprv_from_mnemonic(mnemonic, lang, passphrase, prefix, path, network)
return _derive_xkeys_from_xkey(xprv, index_start, index_end, change)


def bip32_derive_xkeys_from_xkey(xkey: Union[Xprv, Xpub],
index_start: Union[str, int],
index_end: Union[str, int],
path: str = BIP32_DERIVATION_PATH,
change: Union[str, int] = 0) -> List[Union[Xprv, Xpub]]:
"""
Derive a range of extended keys from Xprv and Xpub keys using BIP32 path structure.

Args:
xkey: Parent extended key (Xprv or Xpub)
index_start: Starting index for derivation
index_end: Ending index for derivation (exclusive)
path: Base derivation path (default: BIP32_DERIVATION_PATH)
change: Change level (0 for receiving addresses, 1 for change addresses)

Returns:
List[Union[Xprv, Xpub]]: List of derived extended keys
"""
# Convert index arguments to integers
start_idx = step_to_index(index_start) if isinstance(index_start, str) else index_start
end_idx = step_to_index(index_end) if isinstance(index_end, str) else index_end

# Validate indices
if start_idx < 0 or end_idx < 0 or start_idx >= end_idx:
raise ValueError("Invalid index range: start must be non-negative and less than end")

# Parse the base path and reconstruct with change value
base_path = path.rstrip('/') # Remove trailing slashes if any
if base_path.startswith('m/'):
# For absolute paths
derived_path = f"{base_path}/{change}"
else:
# For relative paths
derived_path = f"./{change}"

# First derive to the change level
change_level = ckd(xkey, derived_path)

# Then derive the range of addresses
derived_keys = []
for i in range(start_idx, end_idx):
child_key = change_level.ckd(i)
derived_keys.append(child_key)

return derived_keys
105 changes: 87 additions & 18 deletions bsv/hd/bip44.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,93 @@
from typing import Union, List

from .bip32 import Xprv, Xpub, step_to_index, ckd
from .bip39 import seed_from_mnemonic
from .bip32 import Xprv, Xpub, step_to_index, bip32_derive_xprv_from_mnemonic, bip32_derive_xkeys_from_xkey
from ..constants import Network, BIP44_DERIVATION_PATH


def derive_xprv_from_mnemonic(mnemonic: str,
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP44_DERIVATION_PATH,
network: Network = Network.MAINNET) -> Xprv:
def bip44_derive_xprv_from_mnemonic(mnemonic: str,
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP44_DERIVATION_PATH,
network: Network = Network.MAINNET) -> Xprv:
"""
Derives extended private key using BIP44 format- it is a subset of BIP32.
Inherits from BIP32, only changing the default path value.
"""
return bip32_derive_xprv_from_mnemonic(
mnemonic=mnemonic,
lang=lang,
passphrase=passphrase,
prefix=prefix,
path=path,
network=network
)


def bip44_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 = BIP44_DERIVATION_PATH,
change: Union[str, int] = 0,
network: Network = Network.MAINNET) -> List[Xprv]:
"""
Derive a range of extended keys from a nmemonic using BIP44 format
"""

xprv = bip44_derive_xprv_from_mnemonic(mnemonic, lang, passphrase, prefix, path, network)
return _derive_xkeys_from_xkey(xprv, index_start, index_end, change)


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]]:
"""
derive the subtree root extended private key from mnemonic and path
this function is internal use only within bip44 module
"""
seed = seed_from_mnemonic(mnemonic, lang, passphrase, prefix)
master_xprv = Xprv.from_seed(seed, network)
return ckd(master_xprv, path)
change_xkey = xkey.ckd(step_to_index(change))
return [change_xkey.ckd(i) for i in range(step_to_index(index_start), step_to_index(index_end))]


# [DEPRECATED]
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]]:
"""
derive extended keys according to path "./change/index"
[DEPRECATED] Use bip32_derive_xkeys_from_xkey instead.
This function name is kept for backward compatibility.
"""
change_xkey = xkey.ckd(step_to_index(change))
return [change_xkey.ckd(i) for i in range(step_to_index(index_start), step_to_index(index_end))]
return _derive_xkeys_from_xkey(xkey=xkey,
index_start=index_start,
index_end=index_end,
change=change)


# [DEPRECATED]
def derive_xprv_from_mnemonic(mnemonic: str,
lang: str = 'en',
passphrase: str = '',
prefix: str = 'mnemonic',
path: str = BIP44_DERIVATION_PATH,
network: Network = Network.MAINNET) -> Xprv:
"""
[DEPRECATED] Use bip44_derive_xprv_from_mnemonic instead.
This function name is kept for backward compatibility.
"""
return bip44_derive_xprv_from_mnemonic(
mnemonic=mnemonic,
lang=lang,
passphrase=passphrase,
prefix=prefix,
path=path,
network=network
)


# [DEPRECATED]
def derive_xprvs_from_mnemonic(mnemonic: str,
index_start: Union[str, int],
index_end: Union[str, int],
Expand All @@ -39,5 +97,16 @@ def derive_xprvs_from_mnemonic(mnemonic: str,
path: str = BIP44_DERIVATION_PATH,
change: Union[str, int] = 0,
network: Network = Network.MAINNET) -> List[Xprv]:
xprv = derive_xprv_from_mnemonic(mnemonic, lang, passphrase, prefix, path, network)
return derive_xkeys_from_xkey(xprv, index_start, index_end, change)
"""
[DEPRECATED] Use bip44_derive_xprvs_from_mnemonic instead.
This function name is kept for backward compatibility.
"""
return bip44_derive_xprvs_from_mnemonic(mnemonic=mnemonic,
index_start=index_start,
index_end=index_end,
lang=lang,
passphrase=passphrase,
prefix=prefix,
path=path,
change=change,
network=network)
2 changes: 2 additions & 0 deletions tests/test_hd.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,5 @@ def test_derive():

with pytest.raises(AssertionError, match=r"can't make hardened derivation from xpub"):
derive_xkeys_from_xkey(xpub, "0'", "1'")


56 changes: 56 additions & 0 deletions tests/test_hd_bip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest

from bsv.hd.bip32 import master_xprv_from_seed, bip32_derive_xprvs_from_mnemonic, bip32_derive_xkeys_from_xkey
from bsv.hd.bip39 import seed_from_mnemonic
from bsv.hd.bip44 import bip44_derive_xprvs_from_mnemonic

from bsv.constants import BIP32_DERIVATION_PATH, BIP44_DERIVATION_PATH

# BIP32_DERIVATION_PATH = "m/"
# BIP44_DERIVATION_PATH = "m/44'/236'/0'"

def test_key_derivation_consistency():
# Test mnemonic phrase
test_mnemonic = "skin index hair zone brush soldier airport found stuff rare wonder physical"

# Generate seed from mnemonic
seed = seed_from_mnemonic(test_mnemonic, lang='en')

# Generate master keys
master_xprv = master_xprv_from_seed(seed)
master_xpub = master_xprv.xpub()

# Key derivation using different methods
# 1. BIP32 derivation from master extended private key
keys_from_bip32_xprv = bip32_derive_xkeys_from_xkey(master_xprv, 0, 2, BIP32_DERIVATION_PATH, 0)
# 2. BIP32 derivation from master extended public key
keys_from_bip32_xpub = bip32_derive_xkeys_from_xkey(master_xpub, 0, 2, BIP32_DERIVATION_PATH, 0)
# 3. BIP32 derivation directly from mnemonic
keys_from_bip32_mnemonic = bip32_derive_xprvs_from_mnemonic(test_mnemonic, 0, 2, path=BIP32_DERIVATION_PATH, change=0)

# Test BIP32 derivation consistency
for i in range(2):
assert keys_from_bip32_xprv[i].address() == keys_from_bip32_xpub[i].address(), \
f"BIP32 xprv/xpub derivation mismatch at index {i}"
assert keys_from_bip32_xprv[i].address() == keys_from_bip32_mnemonic[i].address(), \
f"BIP32 xprv/mnemonic derivation mismatch at index {i}"

# Test BIP44 derivation
keys_from_bip32_mnemonic = bip32_derive_xprvs_from_mnemonic(test_mnemonic, 0, 2, path=BIP44_DERIVATION_PATH, change=0)
keys_from_bip44_mnemonic = bip44_derive_xprvs_from_mnemonic(test_mnemonic, 0, 2, path=BIP44_DERIVATION_PATH, change=0)

# Test BIP44 derivation consistency
for i in range(2):
assert keys_from_bip32_mnemonic[i].address() == keys_from_bip44_mnemonic[i].address(), \
f"BIP32/BIP44 derivation mismatch at index {i}"

def test_invalid_mnemonic():
with pytest.raises(ValueError):
invalid_mnemonic = "invalid mnemonic phrase"
bip32_derive_xprvs_from_mnemonic(invalid_mnemonic, 0, 2, path=BIP32_DERIVATION_PATH, change=0)

def test_invalid_derivation_path():
test_mnemonic = "skin index hair zone brush soldier airport found stuff rare wonder physical"
with pytest.raises(ValueError):
invalid_path = "m/invalid"
bip32_derive_xprvs_from_mnemonic(test_mnemonic, 0, 2, path=invalid_path, change=0)