btcsuite / btcutil

Provides bitcoin-specific convenience functions and types
477 stars 410 forks source link

BIP49 (SegWit) Address Generation Support #105

Closed Chillance closed 2 years ago

Chillance commented 7 years ago

After spending a few hours on this, it seems to me that something is missing so I can't generate proper BIP49 addresses. Unless I missed something. I use hdkeychain.NewKeyFromString to generate (derived) addresses. Works fine for BIP44. But now I'm trying to generate SegWit addresses.

It seems I need to be able to set the BIP32 Derivation path to: m/49'/1'/0'/0, or, well m/49' something at least as currently m/44' seems to be hard-coded somewhere. I have the proper "Account Extended Public Key", but since I can't change the "Purpose", the generated addresses ends up different from what I expect to get.

Anyone here that can help me out here?

Thanks!

davecgh commented 7 years ago

I'd have to see your code to be sure, but nothing in hdkeychain generates a specific path or hard codes it, rather that is something you do in the caller. Based on your comment, it sounds like you're trying to load in an extended public key derived with the BIP44 sceme, and then trying to "go back up" to change a purpose. There is no way to do that from a given child derivation as hardened children are specifically designed to prevent that.

Instead, you need to start with the master extended key, and then derive 49'/1'/0'/0.

For example, consider the following:

package main

import(
    "bytes"
    "fmt"

    "github.com/btcsuite/btcd/chaincfg"
    "github.com/btcsuite/btcutil/hdkeychain"
)

func main(){
    fakeSeed := bytes.Repeat([]byte{0x00}, 16)
    master, err := hdkeychain.NewMaster(fakeSeed, &chaincfg.MainNetParams)
    if err != nil {
        panic(err)  
    }

    // m/49'
    purpose, err := master.Child(49 + hdkeychain.HardenedKeyStart)
    if err != nil {
        panic(err)  
    }

    // m/49'/1'
    coinType, err := purpose.Child(1 + hdkeychain.HardenedKeyStart)
    if err != nil {
        panic(err)  
    }

    // m/49'/1'/0'
    acct0, err := coinType.Child(0 + hdkeychain.HardenedKeyStart)
    if err != nil {
        panic(err)  
    }

    // m/49'/1'/0'/0
    acct0External, err := acct0.Child(1)
    if err != nil {
        panic(err)  
    }

    acct0ExternalPub, err := acct0External.Neuter()
    if err != nil {
        panic(err)  
    }

    fmt.Println(acct0ExternalPub)

    // Show that starting with the correct extended public key string
    // is not modified.
    fmt.Println(hdkeychain.NewKeyFromString(acct0ExternalPub.String()))
}

Output:

xpub6ErAwkSsGjZB5AJ6kEmdHLBT953p3NikFcEgFuSVggz1LUCp1JXqtwEcumJR1WMBoY92am573ac8VXXTTGj46Q6HCzFzZBkZHFCjU4UtWSK
xpub6ErAwkSsGjZB5AJ6kEmdHLBT953p3NikFcEgFuSVggz1LUCp1JXqtwEcumJR1WMBoY92am573ac8VXXTTGj46Q6HCzFzZBkZHFCjU4UtWSK
Chillance commented 7 years ago

So, I get this panic: panic: cannot derive a hardened key from a public key which means I can't do the 49 + hdkeychain.HardenedKeyStart part.

I use testnet so I can share the code for it. So, this work fine:

    masterKey, err = hdkeychain.NewKeyFromString("tpubDCTeyz8KLiDVwexXoYmfDSMRmooGoMrKKrrhXJMhZxtWqm63Y6dbaDaYaEd99dgp6w2b9miDEK6Z7f1qcmbCshEkx7WMgJGkVJtDCdiEarh")
    if err != nil {
        panic(err)
    }
    acct0Ext, err := masterKey.Child(0)
    if err != nil {
        panic(err)
    }
    acct0Ext1, err := acct0Ext.Child(0)
    if err != nil {
        panic(err)
    }

    params := chaincfg.TestNet3Params
    acct0ExtAddr1, err := acct0Ext1.Address(&params)
    if err != nil {
        panic(err)
    }
    fmt.Println("Account 0 External Address 1:", acct0ExtAddr1)

I get: mkKNecT6fGfzb8iL9YmjMDHbaL87eLjFFz (this is correct for BIP44)

I even added data into https://iancoleman.github.io/bip39/ and noticed that the "Account Extended Public Key" I use changes for BIP49. Then it's: tpubDCFUZ43iCUBBTJnMv2nHT4wTuNpDwguV4pbCgDKxbRijNF4N9fWGXNARD22w2AcjHDnzs9SkriSwHS5piRVm91tNMtJywpJwuEY1pt2ioFD And this is fine, because this is exactly the same that Trezor is providing me and I'm trying to get the proper SegWit addresses from. And this seems to be derived from BIP49, so shouldn't it work?

But, if I use that in the above code, the address I get is: mmVn5JSRUZcWgxJJnAVSX7rGxt1fwbaXb3 and NOT: 2N3HSFUtaaqm5aaWboxup3uppGp4Z7HrcCP which I should get. So, something is missing.

Chillance commented 7 years ago

Oh, and feel free to use these on that bip39 site: https://iancoleman.github.io/bip39/ to see what I mean (don't forget to select BitCoin Testnet under "Coin"): wish best bamboo toward thrive parade relief denial tissue okay limb clock end sketch unique train rib there viable canvas nothing napkin post enough

davecgh commented 7 years ago

Alright, I understand your issue now. It appears that you are conflating things here, so I was having trouble following you. All an extended public key does is give you a way to deterministically derive the actual underlying EC pubkey. Extended keys and addresses are entirely different things.

When you talk about mkKNecT6fGfzb8iL9YmjMDHbaL87eLjFFz being correct for BIP44, that really isn't the case. Rather, it is a base58 non-segwit pay-to-pubkey-hash style address for the derivation path of m/49'/1'/0'/0/0.

The Address function of the hdkeychain is just a convenience function to create a base58 non-segwit pay-to-pubkey-hash style address using the underlying EC pubkey for the given extended public key. What you really are after is creating a new base58 segwit pay-to-script-hash style address, where the script hash is the hash of the script per the specification in BIP49, and for which the package does not provide a convenience function.

It's trivial to do though. You'd just need to follow BIP49 in order to generate the script and use btcutil.NewAddressScriptHash with that generated script. You can get the underlying EC public key with .ECPubKey and use btcutil.Hash160 to get the HASH160 of it for use in generating the script.

package main

import (
    "fmt"

    "github.com/btcsuite/btcd/chaincfg"
    "github.com/btcsuite/btcd/txscript"
    "github.com/btcsuite/btcutil"
    "github.com/btcsuite/btcutil/hdkeychain"
)

func main() {
    // m/49'/1'/0'
    acct0Pub, err := hdkeychain.NewKeyFromString("tpubDCFUZ43iCUBBTJnMv2nHT4wTuNpDwguV4pbCgDKxbRijNF4N9fWGXNARD22w2AcjHDnzs9SkriSwHS5piRVm91tNMtJywpJwuEY1pt2ioFD")
    if err != nil {
        panic(err)
    }

    // m/49'/1'/0'/0
    acct0ExternalPub, err := acct0Pub.Child(0)
    if err != nil {
        panic(err)
    }

    // m/49'/1'/0'/0/0
    acct0External0Pub, err := acct0ExternalPub.Child(0)
    if err != nil {
        panic(err)
    }

    // BIP49 segwit pay-to-script-hash style address.
    pubKey, err := acct0External0Pub.ECPubKey()
    if err != nil {
        panic(err)
    }
    keyHash := btcutil.Hash160(pubKey.SerializeCompressed())
    scriptSig, err := txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(keyHash).Script()
    if err != nil {
        panic(err)
    }
    acct0ExtAddr0, err := btcutil.NewAddressScriptHash(scriptSig, &chaincfg.TestNet3Params)
    if err != nil {
        panic(err)
    }
    fmt.Println(acct0ExtAddr0)
}

Output:

2N3HSFUtaaqm5aaWboxup3uppGp4Z7HrcCP
Chillance commented 7 years ago

Oh, wow. That was certainly not something obvious and straight forward to do for me (who comes up with all this stuff? :) ), but thanks a lot for the help!

Maybe there should be a convenience function for this? Even though it isn't all that extra code to write, I can imagine this will become more popular to do.

davecgh commented 7 years ago

It might be worth adding a convenience function in btcutil. I'm actually of the mind that Address should probably be removed from the extended pubkey type however since, as you noted, it will become more and more confusing as different address schemes are devised. The only reason it is there to begin with is to provide a slight optimization when the underlying serialized public key bytes are already known without exposing them to the caller which could potentially mutate them.

davecgh commented 7 years ago

I'll go ahead and leave this open in case somebody wants to make the aforementioned modifications and so it doesn't get forgotten.

hiroki-money commented 6 years ago

+1

bestplay commented 5 years ago

+1

bjarnemagnussen commented 5 years ago

I have created a small commit which adds the functionality, feel free to use it (and suggestions for improvements are of course very welcome!). However, I also agree with @davecgh that at some point with more and more different address/script types it may actually be the wrong place to add those functions to.

Sexual commented 5 years ago

Any reason this hasn't been merged?

stakost commented 4 years ago

@davecgh Dave, could you explain how to generate the BIP49 public key?

From example above, how can I get Account Extended Public Key upub5DP47pDH39bHdd29xmSKctKb4MHE8SRvmrSEyUbA7VqCTewyKUAVWjXAuN5PVwhjGPug199CwdZEwPkXAUQUrsPGhGUQWZCFW6kfu6aNZb1 for path m/49'/1'/0'/0

Because basic method .Neuter() did not working :( with error message unknown hd private extended key bytes

Thanks!

sammy007 commented 4 years ago

If this patch is fine, could you merge it?