A question regarding automated derivation of addresses using xpub and path for Zcash

694 Views Asked by At

I'm currently developing an API for my company which utilizes xpub and path to generate an address.

Thing is, I managed to do that for all cryptocurrencies we require except for Zcash. We're using trezor hardware wallet and Trezor connect doesn't work for us, since we need the flow to be detached from device itself.

Generally, I'm using bitcoinjs library for all other coins, yet I couldn't derive the right address format for zcash. Spent quite a lot of time searching in github issues, but no luck. A word of help would be really great, thanks guys! My code currently looks like this:

xpub = process.env.ZEC_PUB_KEY;
network = {
messagePrefix: '\x18ZCash Signed Message:\n',
bech32: 't1',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4
},
pubKeyHash: 0x1cb8,
scriptHash: 0x1cbd,
wif: 0x80
};
p2wpkh = bjs.payments.p2wpkh({ pubkey: bjs.bip32.fromBase58(xpub, network).derive(0).derive(pathNumber).publicKey, network });
payment = bjs.payments.p2sh({ redeem: p2wpkh, network });
address = payment.address;
3

There are 3 best solutions below

0
On

So, turns out I've found the answer, required some reading on weird resources. Hope this will help you.

First thing, zcash is not SegWit compatible, therefore the p2wpkh function won't work for this, you should use p2pkh. The p2sh method call is also redundant here, so the code will look something like this:

network = {
messagePrefix: '\x18ZCash Signed Message:\n',
bech32: 't1',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4
},
pubKeyHash: 0x1cb8,
scriptHash: 0x1cbd,
wif: 0x80
};
payment= bjs.payments.p2pkh({ pubkey: bjs.bip32.fromBase58(xpub, network).derive(0).derive(pathNumber).publicKey, network });
address = payment.address;

Also, bitcoinjs-lib library doesn't support altcoins in some ways. In this case, the p2pkh function utilizes toUint8 in it's core, so you can basically fork the repo and update that method to use a data type of broader boundaries, toUint16LE.

Afterwards, the function will return a bitcoin address, but we need zcash. We can easily convert it by using the following function:

function baddrToTaddr(baddr_str) {
var baddr = bs58check.decode(baddr_str).slice(1);  // discard type byte
var taddr = new Uint8Array(22);
taddr.set(baddr, 2);
taddr.set([0x1c,0xb8], 0);  // set zcash type bytes
return bs58check.encode(Buffer.from(taddr));
}

That's it.

0
On

I used the solution first posted by Lasha Lomidze and built on top of that a little bit. I had a requirement to keep the default package the same, so I had to recreate the hash160 function used. I was then able to simplify the P2PKH address script function a little bit see below. Notice I use writeUInt16BE to correctly write the pubKeyHash vs writeUInt16LE due to the order in which the bytes are written:

import * as crypto from './utils/crypto'
import * as ecc from 'tiny-secp256k1'
import BIP32Factory from 'bip32'
import { Network } from 'bitcoinjs-lib'


const bs58check = require('bs58check')
const bip32 = BIP32Factory(ecc)

const ZCASH: Network = {
    messagePrefix: '\x18ZCash Signed Message:\n',
    bech32: 't1',
    bip32: {
        public: 0x0488b21e,
        private: 0x0488ade4
    },
    pubKeyHash: 0x1cb8,
    scriptHash: 0x1cbd,
    wif: 0x80
}

function deriveAddr (xpub: string, childNumber: number, changeIndex = 0): string {
    const node = bip32.fromBase58(xpub,ZCASH)
    const derivedKey = node.derive(changeIndex).derive(childNumber)

    return getP2PKHAddress2BytePubKeyHash(derivedKey.publicKey,ZCASH)
}

function getP2PKHAddress2BytePubKeyHash(pubkey: Buffer, network: Network): string{
    // allocate 22 bytes instead of bitcoinlib-js 21
    const payload = Buffer.allocUnsafe(22)
    // write UInt16BE to make use of the network
    // pubKeyHash
    payload.writeUInt16BE(network.pubKeyHash, 0)

    // hash the pubkey and copy starting at position 2
    // because the writeUInt16BE writes to two bytes
    const hashVal = crypto.hash160(pubkey)
    hashVal.copy(payload, 2)

    // return payload with bs58 encoding
    return bs58check.encode(payload)
}

// should derive t1dm4Nvu9QrQk3nRSxosmuyoVms8c2HSpMg
console.log(deriveAddr('xpub6DXNsrgyCs9dEAmjMBSXmZUFajUHHg6d6eT1TxT3egPMb4r4Aka5CLFtdW4eCd6Z3N4P8cHKiuUDeUauz25PfdzFZuvmnjtonUfQoCvoztj', 0, 0))

The below items I put in a file called crypto.ts in the utils directory and exported the hash160 function to be used by the above function.

export {}
const createHash = require('create-hash')

function ripemd160(buffer: Buffer): Buffer {
    try {
      return createHash('rmd160').update(buffer).digest()
    } catch (err) {
      return createHash('ripemd160').update(buffer).digest()
    }
}
function sha256(buffer: Buffer): Buffer {
    return createHash('sha256').update(buffer).digest()
}

export function hash160(buffer: Buffer): Buffer {
    return ripemd160(sha256(buffer))
}
0
On

Lasha /Zack, thank you both your replies above, you've put an end to a couple of days of frustration for me as I ran into the same problem with no obvious answer. For me, I was trying to get some "old" code working with newer libraries that weirdly didn't seem to support this out of the box. In the end just updating my code to use the getP2PKHAddress2BytePubKeyHash function worked perfectly.

C'est tout. :)