Skip to content

address <-> pubkey-hash wrappers don't round-trip (format mismatch: hash160 vs scriptPubKey) #1

Description

@xanimo

python-libdogecoin: address <-> pubkey-hash wrappers don't compose

The Python wrappers for converting between a p2pkh address and its
pubkey-hash form do not round-trip, because two similarly-named wrappers
return/expect different data formats for what they both call a
"pubkey hash."

Observed

import libdogecoin as l
l.w_context_start()
priv, addr = l.w_generate_priv_pub_key_pair(0, False)

hash_form   = l.w_dogecoin_address_to_pubkey_hash(addr)
# -> 20-byte hash160, e.g. "8350f452...d6886112" (40 hex chars)

script_form = l.w_dogecoin_private_key_wif_to_pubkey_hash(priv)
# -> 25-byte P2PKH scriptPubKey, e.g. "76a9148350f452...88ac" (50 hex chars)

l.w_get_addr_from_pubkey_hash(hash_form,   False)  # WRONG address
l.w_get_addr_from_pubkey_hash(script_form, False)  # correct -> addr
l.w_context_stop()

The natural-looking round trip
addr -> w_dogecoin_address_to_pubkey_hash -> w_get_addr_from_pubkey_hash
returns a wrong address, because:

  • w_dogecoin_address_to_pubkey_hash returns a bare 20-byte hash160.
  • w_get_addr_from_pubkey_hash wraps the C function getAddrFromPubkeyHash,
    which expects a full 25-byte scriptPubKey (76a914<hash>88ac). It strips
    the P2PKH opcodes internally and reads the hash from offset 3.

So the two wrappers are not inverses, despite their names suggesting they are.

Root cause (not a C bug)

The underlying C is internally consistent and correct. Verified directly:

generatePrivPubKeypair(wif, addr, false);
char res[51];
dogecoin_p2pkh_address_to_pubkey_hash(addr, res);   // scriptPubKey
char back[100];
getAddrFromPubkeyHash(res, false, back);            // scriptPubKey -> addr
// back == addr  (MATCH)

getAddrFromPubkeyHash's contract is scriptPubKey -> address, as the C
test suite confirms (test/transaction_tests.c feeds it the output of
dogecoin_p2pkh_address_to_pubkey_hash, which is a scriptPubKey, and asserts
it round-trips to the original address). The pubkey_hash[PUBKEYHASHLEN]
parameter name is misleading but the behavior is correct and tested.

The mismatch is purely at the binding layer: it exposes a function that
returns a hash160 (w_dogecoin_address_to_pubkey_hash) alongside one that
consumes a scriptPubKey (w_get_addr_from_pubkey_hash) without making the
format difference visible, so users who pair them get wrong results silently.

Suggested fixes (any of)

  1. Make w_get_addr_from_pubkey_hash accept a bare hash160 (the format its
    name implies) by wrapping a hash160->address path in the binding, e.g.
    build the scriptPubKey from the hash before calling, or call a hash160
    helper directly. This makes the round trip with
    w_dogecoin_address_to_pubkey_hash work as users expect.
  2. Rename / document the wrappers to make the format explicit, e.g.
    w_scriptpubkey_to_address vs a separate w_hash160_to_address, and note
    in docstrings exactly which 20- vs 25-byte hex each produces/consumes.
  3. Add a round-trip test at the binding level
    (addr -> hash -> addr and addr -> scriptpubkey -> addr) so the format
    mismatch is caught in CI.

Note

The three *_to_pubkey_hash wrappers return inconsistent formats for the same
key (w_dogecoin_address_to_pubkey_hash -> 20-byte hash160;
w_dogecoin_private_key_wif_to_pubkey_hash -> 25-byte scriptPubKey). Aligning
or clearly documenting these would prevent the same confusion elsewhere.

Affects

Reproduced against libdogecoin==0.1.5rc1 (PyPI, built from v0.1.5-pre).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions