How to derive an Orchard address from a seed phrase in rust?

I have built a small rust app that generates a seed phrase and from that derives a Sapling address. How can I build on this to derive an Orchard address as well?

Also, the sapling address I generate does not match the one generated (with the same seed phrase) by ZEC wallet apps such as YWallet. How should I change my key derivation so the address matches?

Thanks.

use zcash_client_backend::encoding::encode_payment_address;
use zcash_primitives::zip339::{Count, Mnemonic};
use zcash_primitives::zip32::{ExtendedSpendingKey, ChildIndex};
use zcash_primitives::consensus::Network::MainNetwork;
use zcash_primitives::consensus::Parameters;
use zcash_client_backend::address::UnifiedAddress;
use orchard::Address;
use std::io;

fn main() {
    let mnemonic = Mnemonic::generate(Count::Words12);
    let phrase = mnemonic.phrase();
    println!("Mnemonic phrase: {}", phrase);

    let seed = mnemonic.to_seed("");
    let spending_key = ExtendedSpendingKey::master(&seed);
    let (_ , payment_address) = spending_key
        .derive_child(ChildIndex::Hardened(44))
        .derive_child(ChildIndex::Hardened(133))
        .derive_child(ChildIndex::Hardened(0))
        .derive_child(ChildIndex::NonHardened(0))
        .derive_child(ChildIndex::NonHardened(0))
        .default_address();
    let address = encode_payment_address(MainNetwork.hrp_sapling_payment_address(), &payment_address);
    println!("Zcash address: {:?}", address);
}
3 Likes

The orchard crate provides similar APIs in the orchard::zip32 module. You can then combine the results into a Unified address type, and use the zcash_address crate to encode it. We have a UnifiedAddress type in the zcash_client_backend crate that handles the latter (we’re planning to refactor it in the near future to create a full one-stop solution for your use case).

You appear to be using the BIP 44 key path, which includes two non-hardened derivation levels. Per ZIP 32, Sapling by default does not use any non-hardened derivation levels (they are unnecessary, as you can use diversifiers to get multiple addresses for a specific account). Remove those, and you should get the same result.

5 Likes

It is also 32 and not 44.

Are you sure?

BIP 32 defines the general mechanism for key derivation of Bitcoin-style transparent keys and addresses. It also specifies the key path m / account' / change / address_index, that was used by Bitcoin wallets between 2012 and 2014.

BIP 44 defines the standard key path for Bitcoin-style transparent wallets of m / purpose' / coin_type' / account' / change / address_index used by Bitcoin wallets from 2014 onwards. This is the key path that we use for transparent Zcash addresses, and that @aarnott is using above.

1 Like

I meant that if he wants to match the YWallet & ZecWallet z-addr, he needs to use Zip-32 and besides what you said about omitting the non-hardened path, he has to use 32 and not 44.

m/32’/133’/0’ instead of m/44’/133’/0’/0/0

2 Likes

Ah okay, you were referencing the purpose field of the code sample, not the BIPs. Indeed, that also needs to be set matching ZIP 32 :+1:

1 Like

Thank you both! The m/32'/133'/0' path did indeed get my rust program to replicate a Sapling address with one produced by a wallet app with the same seed phrase. That’s exciting progress.

I’m struggling to see how to take that same seed phrase and produce an Orchard address.
This would be cheating probably, but… I see that the sapling receiver and orchard receiver have the same format. Are they compatible, such that I could actually extract the receiver from the sapling address and simply re-encode it into an orchard address? Or are the internal mechanics of address derivation different?

1 Like

The functionality for orchard-specific operations is in the orchard crate, which is in a separate github repo from everything not orchard-specific. orchard::keys::SpendingKey has a from_zip32_seed associated function which should be the missing link.

1 Like

Thanks. This part seems easy enough:

let o = SpendingKey::from_zip32_seed(&seed, COIN_TYPE, 0).expect("Failed to derive Orchard key from seed.");

But I can’t figure out how to turn that into an address.

Something like:

    let fvk = ::orchard::keys::FullViewingKey::from(&o);
    let a = fvk.address_at(0u64, Scope::External);
    let ua = ::zcash_client_backend::address::UnifiedAddress::from_receivers(Some(a), None, None).unwrap();
    let ua_str = ua.encode(&MainNetwork);
3 Likes

That worked. Thank you!

For the interested, here is the program that generates a new seed and derives sapling and orchard addresses from it:

use zcash_client_backend::address::UnifiedAddress;
use zcash_client_backend::encoding::encode_payment_address;
use zcash_primitives::zip339::{Mnemonic, Count};
use zcash_primitives::zip32::{ExtendedSpendingKey, ChildIndex};
use zcash_primitives::consensus::Network::MainNetwork;
use zcash_primitives::consensus::Parameters;
use orchard::keys::{SpendingKey, FullViewingKey, Scope};

fn main() {
    const PURPOSE: u32 = 32;
    const COIN_TYPE: u32 = 133;
    let account = 0;

    let mnemonic = Mnemonic::generate(Count::Words12);
    let phrase = mnemonic.phrase();
    println!("Mnemonic phrase: {}", phrase);

    let seed = mnemonic.to_seed("");

    let spending_key = ExtendedSpendingKey::master(&seed);
    let (_ , payment_address) = spending_key
        .derive_child(ChildIndex::Hardened(PURPOSE))
        .derive_child(ChildIndex::Hardened(COIN_TYPE))
        .derive_child(ChildIndex::Hardened(account))
        .default_address();
    let sapling_address = encode_payment_address(MainNetwork.hrp_sapling_payment_address(), &payment_address);
    println!("Zcash address:           {:?}", sapling_address);

    let orchard_sk = SpendingKey::from_zip32_seed(&seed, COIN_TYPE, account).expect("Failed to derive Orchard key from seed.");
    let fvk = FullViewingKey::from(&orchard_sk);
    let a = fvk.address_at(0u64, Scope::External);
    let orchard_address = UnifiedAddress::from_receivers(Some(a), None, None).unwrap();
    let orchard_address_str = orchard_address.encode(&MainNetwork);

    println!("Orchard address:         {:?}", orchard_address_str);

    let combined_ua = UnifiedAddress::from_receivers(Some(a), Some(payment_address), None).unwrap();
    println!("Combined address:        {:?}", combined_ua.encode(&MainNetwork));
}

It might be interesting to add transparent address derivation to this, but I already have that working in C# so I don’t expect to need to have rust do that part for me.

3 Likes