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.

Poseidon
  • 599
  • 2
  • 20
  • The checksum covers the network type byte + the public key hash bytes. Your code only hashes the network type byte. – Pieter Wuille Jan 11 '23 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 '23 at 20:46
  • Ah, apologies, I misunderstood. Maybe the issue is that you're copying 8 checksum *characters*, rather than 4 checksum *bytes*? – Pieter Wuille Jan 11 '23 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 '23 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. – Pieter Wuille Jan 12 '23 at 13:52
  • I believe I was trying out both methods of hashing the hex string and hashing the byte output however it seems as if the problem was in the sha256 and ripemd hashes themselves. The openssl versions used in bitcoin apparently `will convert the hex string into binary and then output it in hexdump style (ascii)` [https://medium.com/coinmonks/how-to-generate-a-bitcoin-address-step-by-step-9d7fcbf1ad0b] (gen-p2pkh-medium) this method produces completely different results than hashing it as a string which was what made me confused and produce wrong checksums. – Poseidon Jan 12 '23 at 20:02

1 Answers1

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);
    }
}
Adam B
  • 334
  • 8
  • 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. [https://medium.com/coinmonks/how-to-generate-a-bitcoin-address-step-by-step-9d7fcbf1ad0b](source) This article uses these SSL flags `xxd -p -r ` which I wouldn't know how to do in rust offhand. – Poseidon Jan 12 '23 at 19:51
  • 1
    @Poseidon **no** SHA256 implementation handles hex input. Whenever you see hex, it's done outside of SHA256 for human interaction. – Pieter Wuille Jan 12 '23 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 '23 at 21:41