2

I have followed the steps to generate a p2pkh compressed base58 address and am consistently checking it to find that I get an invalid checksum.

I am using this python code to check the address: (Checked the solution against the python code and an online address checker, this current python code fails around the encode part for some reason so it cant print out the addr after, it will check the addr still)

import base58
adr = '1AMGLbW71pvdMuRWkDdmZWibmzRkxa6LVt'
adr160 = base58.b58decode_check(adr).encode('hex')[2:]
print(adr160)

Here is the Rust function to hash a public key:

fn hashPubKey(publicKeyStr: String) -> (String, String)
{
    let sha256_1 = digest(publicKeyStr.as_bytes());
    let mut ripemd160 = Ripemd160::new();
    ripemd160.update(sha256_1.as_bytes());
    let ripe = format!("{:x}", ripemd160.finalize());
    let network_byte_key = format!("{:0>42}", ripe.clone());
    let sha256_2 = digest(network_byte_key.as_bytes().clone());
    let sha256_3 = digest(sha256_2.as_bytes());
    let mut checksum = String::new();
    for i in 0..8
    {
        checksum.push(sha256_3.chars().nth(i).unwrap());
    }
    let checkKey = network_byte_key.clone() + &checksum;
    let pubkeyint = big_hex::hex_to_int(checkKey.clone());
    let compressedKey = base58_encode(pubkeyint.clone());
    let pubkeyint_check: BigInt = base58_decode(compressedKey.clone());
    assert!(pubkeyint == pubkeyint_check, "error in base58 encoding/decoding");
    return (ripe, compressedKey);
}

The function returns the hash160 address and the base58 compressed key. It seems to be doing everything that online resources say to do, but it seems as if it generates invalid checksums. I tried to check it against some online tools but they all seemed to generate invalid hashes or inconsistent results with each-other.

6
  • The checksum covers the network type byte + the public key hash bytes. Your code only hashes the network type byte. Jan 11, 2023 at 13:45
  • Do you mean the line let sha256_2 = digest(network_byte_key.as_bytes().clone()); ? The network_byte_key variable that is being hashed here does have the public key hash bytes concatenated on to it. It is done in the previous line: let network_byte_key = format!("{:0>42}", ripe.clone()); This line takes the ripemedhash and pads the network byte to the front. Then that entire hexadecimal is hashed as bytes.
    – Poseidon
    Jan 11, 2023 at 20:46
  • Ah, apologies, I misunderstood. Maybe the issue is that you're copying 8 checksum characters, rather than 4 checksum bytes? Jan 11, 2023 at 20:47
  • I'm having trouble understanding what value I would be targeting even if I were to interpret it as bytes. Example: first four elements in the byte array for sha256: 6ef39a7ee6c9a7436636e1acf97fd6680d4f2a4319ccb9605b4e6d821a4f5cfe are [54, 101, 102, 51] These correspond to 6ef3 which is the first 4 chars of the hash. However if I use this 4 character hex to append to the hash I actually get a 31 character key which also has an invalid checksum. If I then grab the first 8 bytes in the array I am right back to where I started of getting the first 8 chars of the hash. What to do differently?
    – Poseidon
    Jan 12, 2023 at 11:26
  • I guess I'm asking (I don't know Rust): are you hashing hex character bytes, or are you hashing the hash output directly? SHA256 has a 32-byte output, which is often represented as 64 hex characters for human consumption. However, here you must use the bytes directly; a hex conversion (which the sha256 functionality may be doing for you) should never be involved. Jan 12, 2023 at 13:52

1 Answer 1

3

I post Rust code below which does what you try to do. I am not sure what's wrong with your code, but I hope my code helps.

Please note that the example you use, 1AMGLbW71pvdMuRWkDdmZWibmzRkxa6LVt is not a valid address. With valid checksum it would be 1AMGLbW71pvdMuRWkDdmZWibmzRkyByigA.

In my example I use public key 032d1e1736727f0957a5137ab93bfbbf6e0c293dd0f65a6c8947b563c1f5827376, which produces address bytes 007eefa5452e04cbc2d0f949ce96080aa2afc15c314e858c41 (prefix + pubkey hash + checksum), and address 1CaBE3CYxhqdjcxKCPqwNhsys1weUnZhHJ.

I provide 3 solutions: one with 'manual' steps, one where the bas58 checksum is done by the base58 library, and the shortest where bitcoin crate is used.

use bitcoin::hashes::Hash;
use bitcoin::util::address::Address;
use bitcoin;

fn hex_to_bytes(hex: &str) -> Vec<u8> {
    hex::decode(hex).unwrap()
}

fn bytes_to_hex(bytes: &Vec<u8>) -> String {
    hex::encode(bytes)
}

fn pubkey_hash(pubkey: &Vec<u8>) -> Vec<u8> {
    let sha = bitcoin::hashes::sha256::Hash::hash(pubkey);
    bitcoin::hashes::ripemd160::Hash::hash(&sha).to_vec()
}

// All manual steps
fn pubkey_to_addr_detailed(pubkey_hex: &str) -> String {
    let pubkey = hex_to_bytes(pubkey_hex);
    let mut pubkey_hash = pubkey_hash(&pubkey);
    let mut address_bytes = Vec::new();
    let prefix: u8 = 0;
    address_bytes.push(prefix);
    address_bytes.append(&mut pubkey_hash);
    let checksum = bitcoin::hashes::sha256d::Hash::hash(&address_bytes).to_vec();
    let mut checksum_truncated = checksum[0..4].to_vec();
    let mut address_bytes_with_checksum = Vec::new();
    address_bytes_with_checksum.append(&mut address_bytes);
    address_bytes_with_checksum.append(&mut checksum_truncated);
    bitcoin::util::base58::encode_slice(&address_bytes_with_checksum)
}

// Base58 checksum done by base58
fn pubkey_to_addr_base58(pubkey_hex: &str) -> String {
    let pubkey = hex_to_bytes(pubkey_hex);
    let mut pubkey_hash = pubkey_hash(&pubkey);
    let mut address_bytes = Vec::new();
    let prefix: u8 = 0;
    address_bytes.push(prefix);
    address_bytes.append(&mut pubkey_hash);
    bitcoin::util::base58::check_encode_slice(&address_bytes)
}

// Using bitcoin::address
fn pubkey_to_addr_bitcoin(pubkey_hex: &str) -> String {
    let pubkey_data = hex_to_bytes(pubkey_hex);
    let pubkey = bitcoin::PublicKey::from_slice(&pubkey_data).unwrap();
    let addr = Address::p2pkh(&pubkey, bitcoin::Network::Bitcoin);
    addr.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    const PUBKEY1: &str = "032d1e1736727f0957a5137ab93bfbbf6e0c293dd0f65a6c8947b563c1f5827376";
    const ADDRESS: &str = "1CaBE3CYxhqdjcxKCPqwNhsys1weUnZhHJ";

    #[test]
    fn test_pubkey_to_addr_detailed() {
        let addr = pubkey_to_addr_detailed(PUBKEY1);
        assert_eq!(addr, ADDRESS);
    }

    #[test]
    fn test_pubkey_to_addr_base58() {
        let addr = pubkey_to_addr_base58(PUBKEY1);
        assert_eq!(addr, ADDRESS);
    }
    
    #[test]
    fn test_pubkey_to_addr_bitcoin() {
        let addr = pubkey_to_addr_bitcoin(PUBKEY1);
        assert_eq!(addr, ADDRESS);
    }

    #[test]
    fn t1() {
        let pubkey = hex_to_bytes(PUBKEY1);
        let mut pubkey_hash = pubkey_hash(&pubkey);
        assert_eq!(bytes_to_hex(&pubkey_hash), "7eefa5452e04cbc2d0f949ce96080aa2afc15c31");
        let mut address_bytes = Vec::new();
        let prefix: u8 = 0;
        address_bytes.push(prefix);
        address_bytes.append(&mut pubkey_hash);
        assert_eq!(bytes_to_hex(&address_bytes), "007eefa5452e04cbc2d0f949ce96080aa2afc15c31");
        let checksum = bitcoin::hashes::sha256d::Hash::hash(&address_bytes).to_vec();
        assert_eq!(bytes_to_hex(&checksum), "4e858c4104e953987e6147a2a653a423fa63c4f518c89e9d58c3a3aa40f518e3");
        let mut checksum_truncated = checksum[0..4].to_vec();
        assert_eq!(bytes_to_hex(&checksum_truncated), "4e858c41");
        let mut address_bytes_with_checksum = Vec::new();
        address_bytes_with_checksum.append(&mut address_bytes);
        address_bytes_with_checksum.append(&mut checksum_truncated);
        assert_eq!(bytes_to_hex(&address_bytes_with_checksum), "007eefa5452e04cbc2d0f949ce96080aa2afc15c314e858c41");
        let b58 = bitcoin::util::base58::encode_slice(&address_bytes_with_checksum);
        assert_eq!(b58, ADDRESS);
    }
}
3
  • 1
    Thank you sir, changing the libraries being used was a very smart idea. It looks like the SSL library used in bitcoin does not process hex strings the same way my sha256 digest function did. By extension ripemd160 also did not hash properly. That is likely what lead to my results being chaotically wrong. There is some kind of conversion where it takes the hex string and turns it into a binary hex dump. [medium.com/coinmonks/… This article uses these SSL flags xxd -p -r which I wouldn't know how to do in rust offhand.
    – Poseidon
    Jan 12, 2023 at 19:51
  • 1
    @Poseidon no SHA256 implementation handles hex input. Whenever you see hex, it's done outside of SHA256 for human interaction. Jan 12, 2023 at 20:26
  • I meant was I was hashing the hex as a string, the rust sha256 digest function accepts strings and string.as_bytes(). Thank you though for clarifying this though.
    – Poseidon
    Jan 12, 2023 at 21:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.