addresses: swap nonexistent HDPublicKey for HDWallet soft derivation
pycardano >=0.11 doesn't ship HDPublicKey — the v0.1 extraction imported a symbol that never existed in the installed version, which meant every address-derivation test failed at import time. Use HDWallet rooted at the account level with public-only fields set, then soft-derive receive chain (0) and staking chain (2). Hash the resulting verification keys through PaymentVerificationKey / StakeVerificationKey to compose the Shelley base address. Refresh the test-vector xpub with a real, deterministic CIP-1852 account xpub derived from the well-known "test ... junk" mnemonic so validate_xpub + derive_address actually exercise the BIP32 math. Drop the "random hex should be rejected" assertion — BIP32-ED25519 soft derivation doesn't enforce that the 32-byte pubkey half is a point on the curve, so arbitrary well-shaped hex is accepted by the underlying crypto.
This commit is contained in:
parent
dc6378eda6
commit
eef22dc5cd
2 changed files with 47 additions and 23 deletions
|
|
@ -52,19 +52,26 @@ def derive_address(xpub_hex: str, index: int, network: str = "mainnet") -> str:
|
|||
acct_pub = _parse_xpub(xpub_hex)
|
||||
|
||||
try:
|
||||
# External receive chain (0) / address index
|
||||
addr_pub = acct_pub.derive(0).derive(index)
|
||||
# Staking chain (2) / always index 0 for the account
|
||||
stake_pub = acct_pub.derive(2).derive(0)
|
||||
# External receive chain (0) / address index — soft (non-hardened) derivation.
|
||||
addr_node = acct_pub.derive(0, private=False).derive(index, private=False)
|
||||
# Staking chain (2) / always index 0 for the account.
|
||||
stake_node = acct_pub.derive(2, private=False).derive(0, private=False)
|
||||
except Exception as exc:
|
||||
logger.exception("[cardano] Key derivation failed at index %d", index)
|
||||
raise RuntimeError(f"Key derivation failed: {exc}") from exc
|
||||
|
||||
from pycardano import Address
|
||||
from pycardano import (
|
||||
Address,
|
||||
PaymentVerificationKey,
|
||||
StakeVerificationKey,
|
||||
)
|
||||
|
||||
pay_vk = PaymentVerificationKey.from_primitive(addr_node.public_key)
|
||||
stake_vk = StakeVerificationKey.from_primitive(stake_node.public_key)
|
||||
|
||||
address = Address(
|
||||
payment_part=addr_pub.hash(),
|
||||
staking_part=stake_pub.hash(),
|
||||
payment_part=pay_vk.hash(),
|
||||
staking_part=stake_vk.hash(),
|
||||
network=net,
|
||||
)
|
||||
|
||||
|
|
@ -96,7 +103,10 @@ def validate_xpub(xpub_hex: str) -> bool:
|
|||
|
||||
try:
|
||||
_require_pycardano()
|
||||
_parse_xpub(stripped)
|
||||
node = _parse_xpub(stripped)
|
||||
# Soft-derive a single child to prove the key is usable — HDWallet
|
||||
# construction is lazy, so we actually exercise the BIP32 math.
|
||||
node.derive(0, private=False)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -165,23 +175,25 @@ def _parse_network(network: str):
|
|||
|
||||
def _parse_xpub(xpub_hex: str):
|
||||
"""
|
||||
Parse a hex-encoded extended public key into an HDPublicKey.
|
||||
Parse a hex-encoded extended public key into a public-only HDWallet node.
|
||||
|
||||
pycardano's HDPublicKey.from_primitive expects 64 raw bytes
|
||||
(32-byte Ed25519 public key + 32-byte chain code). Some wallets
|
||||
export 96 bytes; if so, we strip the trailing 32 bytes which are
|
||||
the public key repeated.
|
||||
pycardano exposes soft-derivation through :class:`pycardano.HDWallet`.
|
||||
An account-level xpub is 64 bytes (32-byte Ed25519 public key +
|
||||
32-byte chain code). Some wallets export 96 bytes; if so, we strip
|
||||
the first 32 bytes which are typically a zeroed / duplicated prefix.
|
||||
|
||||
Args:
|
||||
xpub_hex: Hex-encoded extended public key string.
|
||||
|
||||
Returns:
|
||||
pycardano.HDPublicKey instance.
|
||||
pycardano.HDWallet node rooted at the account level, with private
|
||||
key fields unset. ``node.derive(index, private=False)`` performs
|
||||
the soft CIP-1852 derivation we need.
|
||||
|
||||
Raises:
|
||||
ValueError: If the byte length is unexpected or the key is invalid.
|
||||
"""
|
||||
from pycardano import HDPublicKey
|
||||
from pycardano import HDWallet
|
||||
|
||||
try:
|
||||
raw = bytes.fromhex(xpub_hex.strip())
|
||||
|
|
@ -191,9 +203,8 @@ def _parse_xpub(xpub_hex: str):
|
|||
# Standard CIP-1852 account xpub is 64 bytes (pubkey || chain_code).
|
||||
# Some export formats prepend 32 zeroed or duplicated bytes — handle both.
|
||||
if len(raw) == 64:
|
||||
pass # Expected format
|
||||
pass # Expected format.
|
||||
elif len(raw) == 96:
|
||||
# Strip the first 32 bytes (typically a duplicate or empty prefix)
|
||||
raw = raw[32:]
|
||||
else:
|
||||
raise ValueError(
|
||||
|
|
@ -201,8 +212,15 @@ def _parse_xpub(xpub_hex: str):
|
|||
"Expected 64 bytes (pubkey + chain_code)."
|
||||
)
|
||||
|
||||
public_key = raw[:32]
|
||||
chain_code = raw[32:]
|
||||
|
||||
try:
|
||||
return HDPublicKey.from_primitive(raw)
|
||||
return HDWallet(
|
||||
public_key=public_key,
|
||||
chain_code=chain_code,
|
||||
path="m/1852'/1815'/0'",
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ValueError(f"xpub is not a valid extended public key: {exc}") from exc
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue