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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions