c++bitcoinlitecoin

Bitcoin SegWit Wallet Address Calculation


Sorry if this is a bit misleading, but I was actually doing this with Litecoin as opposed to Bitcoin, but the algorithm is exactly the same and I'm pretty sure that the answer will be too. In fact, am almost certain when I look, there's going to be the same problem with Bitcoin too. I'm having difficulty generating the correct SegWit address for a given public key as below:

Litecoin address generation (which is the same as Bitcoin)

  1. I take a compressed public key: 03861b83752e0c47cac36fc5980ae8956f41f6d9792a51f68a6bd5f66cc7364b48
  2. SHA256 on the public key.
  3. RipeMD160 the result from 2.
  4. Add a prefix byte, in my case, 0x3a which is the LTC_TESTNET
  5. Double SHA256 result of 4 and add the first four bytes of this result to the end of the RipeMD160.
  6. Finally, Base58 encode.

All seems dandy, right? Out pops the SegWit address: QhQxSZvVDWr3JvoKsYVC6BBW3DqkGhesrF

However, I'm pretty sure this address isn't right. When I import the private key into Electrum-LTC as a (p2wpkh-p2sh:) the address it generates for this private key is: QYyWqgyWSm1AJWph32GnyY7eamG1wUDruk

Now I believe that Electrum-LTC is right and there's something that I'm missing when generating a SegWit address and there's more to address generation than just changing the network prefix. My Public Key Hash is:

e444ac77800cdf904b928fc4642ab6fb6d4d696c

and Electrum-LTC's Public Key Hash is:

87b3e5bf5b2a1381e6549020d245e45b9ac76c82

Since the values are so VERY different, it suggests that the initial SHA256 is not hashing just the public key alone and that I'm missing something. I'm about out of ideas and am hoping someone has the answer and about the only thing I can find in the source, in the chainparams.cpp was this:

base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E};

Anyone know what I might be doing wrong?

Update - Someone asked for the code - so here it is

#include <openssl/sha.h>
#include <openssl/ripemd.h>
#include <cstdint>
#include <iostream>
#include <vector>

std::string base58Encode(const std::vector<uint8_t>& data)
{
    const uint8_t mapping[] = {
      '1', '2', '3', '4', '5', '6', '7', '8',
      '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
      'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q',
      'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
      'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
      'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p',
      'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
      'y', 'z' };

    std::vector<uint8_t> digits((data.size() * 137) / 100);
    size_t digitslen = 1;
    for (size_t i = 0; i < data.size(); i++)
    {
        uint32_t carry = static_cast<uint32_t>(data[i]);
        for (size_t j = 0; j < digitslen; j++)
        {
            carry = carry + static_cast<uint32_t>(digits[j] << 8);
            digits[j] = static_cast<uint8_t>(carry % 58);
            carry /= 58;
        }
        for (; carry; carry /= 58)
            digits[digitslen++] = static_cast<uint8_t>(carry % 58);
    }
    std::string result;
    for (size_t i = 0; i < data.size() && !data[i]; i++)
        result.push_back(mapping[0]);
    for (size_t i = 0; i < digitslen; i++)
        result.push_back(mapping[digits[digitslen - 1 - i]]);
    return result;
}

std::vector<uint8_t> SHA256_Hash(const std::vector<uint8_t>& data)
{
    std::vector<uint8_t> SHA256_Digest(SHA256_DIGEST_LENGTH);
    SHA256_CTX ctx;
    SHA256_Init(&ctx);
    SHA256_Update(&ctx, data.data(), data.size());
    SHA256_Final(SHA256_Digest.data(), &ctx);
    return SHA256_Digest;
}

std::vector<uint8_t> RipeMD160_Hash(const std::vector<uint8_t>& data)
{
    std::vector<uint8_t> RipeMD160_Digest(RIPEMD160_DIGEST_LENGTH);
    RIPEMD160_CTX ctx;
    RIPEMD160_Init(&ctx);
    RIPEMD160_Update(&ctx, data.data(), data.size());
    RIPEMD160_Final(RipeMD160_Digest.data(), &ctx);
    return RipeMD160_Digest;
}

std::string getWalletAddress(const std::vector<uint8_t>& public_key, uint8_t id)
{
    std::vector<uint8_t> addr_hash = SHA256_Hash(key);
    std::vector<uint8_t> addr_ripe = RipeMD160_Hash(addr_hash);
    addr_ripe.insert(addr_ripe.begin(), id);
    addr_hash = SHA256_Hash(SHA256_Hash(addr_ripe));
    addr_ripe.insert(addr_ripe.end(), addr_hash.begin(), addr_hash.begin() + sizeof(uint32_t));
    return base58Encode(addr_ripe);
}

int main(int argc, char** argv)
{
    const uint8_t LTC_TESTNET = 0x3a;

    std::vector<uint8_t> public_key{
        0x03, 0x86, 0x1b, 0x83, 0x75, 0x2e, 0x0c, 0x47, 0xca, 0xc3, 0x6f, 0xc5, 0x98, 0x0a, 0xe8, 0x95,
        0x6f, 0x41, 0xf6, 0xd9, 0x79, 0x2a, 0x51, 0xf6, 0x8a, 0x6b, 0xd5, 0xf6, 0x6c, 0xc7, 0x36, 0x4b, 0x48};

    std::cout << "Wallet Address: " << getWalletAddress(public_key, LTC_TESTNET) << std::endl;
    // The above ouputs:  QhQxSZvVDWr3JvoKsYVC6BBW3DqkGhesrF
    // But it should be:  QYyWqgyWSm1AJWph32GnyY7eamG1wUDruk
    return 0;
}

Solution

  • Have finally figured this one out... When calculating a p2wpkh-p2sh address, it's not just the prefix of the address that is different. In actual fact, I found the answer to the above problem here: https://bitcoin.stackexchange.com/questions/72775/is-it-possible-to-convert-an-address-from-p2pkh-to-p2sh (although it is quite cryptic in how it is explained).

    The above code in my question will work quite happily for p2pkh address generation for Bitcoin/Litecoin etc, but when generating a Q (for Litecoin), 2 (for Bitcoin), it won't work because it's not just the public key that is hashed. It's actually a script, of 0x00 (DUP_0) and then a length of the public key hash (0x14).

    So, to fix the above code, if the address generation code is changed to:

    std::string getWalletAddress(const std::vector<uint8_t>& public_key, uint8_t id, bool is_p2sh)
    {
        std::vector<uint8_t> addr_hash = SHA256_Hash(key);
        std::vector<uint8_t> addr_ripe = RipeMD160_Hash(addr_hash);
        if (is_p2sh)
        {
            addr_ripe.insert(addr_ripe.begin(), { 0x00, static_cast<uint8_t>(addr_ripe.size()) });
            addr_hash = SHA256_Hash(addr_ripe);
            addr_ripe = RipeMD160_Hash(addr_ripe);
        }
        addr_ripe.insert(addr_ripe.begin(), id);
        addr_hash = SHA256_Hash(SHA256_Hash(addr_ripe));
        addr_ripe.insert(addr_ripe.end(), addr_hash.begin(), addr_hash.begin() + sizeof(uint32_t));
        return base58Encode(addr_ripe);
    }
    
    // This will now work for both p2pkh addresses and p2wpkh-p2sh addresses
    
    getWalletAddress(public_key, LTC_TESTNET, true);  // Produces: QYyWqgyWSm1AJWph32GnyY7eamG1wUDruk