Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Implement Bip32 for seed-phrase/passphrase signing #16942

Merged
merged 8 commits into from
May 4, 2021

Conversation

CriesofCarrots
Copy link
Contributor

@CriesofCarrots CriesofCarrots commented Apr 29, 2021

Problem

Solana non-remote-wallet signers do not support bip32 hierarchical derivation, preventing a lot of interoperability between client tools like solana-cli and 3rd-party wallets.

Summary of Changes

  • Implement solana bip44 derivation for SignerSourceKind::Ask signers

Toward #5246

Todo:

  • Implement bip44 solana derivations
  • Implement query pair for absolute path derivations
  • Add documentation
  • Consider whether to enable any aspect of this for solana-keygen recover not now, reconsider in future

Follow-up work:

  • Rework Keypair serialized format & solana-keygen to store chain code, and support bip32 for File and Stdin signing sources

@Milerius
Copy link

Problem

Solana non-remote-wallet signers do not support bip32 hierarchical derivation, preventing a lot of interoperability between client tools like solana-cli and 3rd-party wallets.

Summary of Changes

* Implement solana bip44 derivation for `SignerSourceKind::Ask` signers

Toward #5246

Todo:

* [x]  Implement bip44 solana derivations

* [ ]  Implement query pair for absolute path derivations

* [ ]  Add documentation

* [ ]  Consider whether to enable any aspect of this for `solana-keygen recover`

Follow-up work:

* Rework Keypair serialized format & `solana-keygen` to store chain code, and support bip32 for File and Stdin signing sources

Excellent until now i was forced to do it this way:

use bip39::core::str::FromStr;
use bip39;
use ed25519_dalek_bip32::ExtendedSecretKey;
use solana_sdk::signature::Signer;

fn main() {
    let derivation_path = ed25519_dalek_bip32::DerivationPath::from_str("m/44'/501'/0'").unwrap();

    let mnemonic = "shoot island position soft burden budget tooth cruel issue economy destroy above";
    let res = bip39::Mnemonic::from_str(mnemonic).unwrap();
    let seed = res.to_seed("");

    let ext = ExtendedSecretKey::from_seed(&seed).unwrap().derive(&derivation_path).unwrap();
    let ref priv_key = ext.secret_key;
    let pub_key = ext.public_key();

    println!("derivation_path: {}", derivation_path);
    println!("priv_key: {:?}", priv_key);
    println!("priv_key len: {:?}", priv_key.as_bytes().len());

    let public_address = bitcoin::util::base58::encode_slice(pub_key.as_bytes());
    println!("public_address: {}", public_address);
    assert_eq!(public_address.len(), 44);
    assert_eq!(public_address, "2bUBiBNZyD29gP1oV6de7nxowMLoDBtopMMTGgMvjG5m");

    let private_key = bitcoin::util::base58::encode_slice(priv_key.as_bytes());
    println!("private key: {}", private_key);

    let pair = ed25519_dalek::Keypair{secret: ext.secret_key, public: pub_key};

    let fin = solana_sdk::signature::keypair_from_seed(pair.to_bytes().as_ref()).unwrap();
    println!("pub_key: {}", fin.pubkey().to_string());
    println!("from_sdk_priv: {}", bitcoin::util::base58::encode_slice(fin.secret().as_bytes()));
}

Looking forward to use the new functions to simplify my approach.

@CriesofCarrots
Copy link
Contributor Author

@t-nelson , do you mind starting to look at this code while I work up some documentation?

Comment on lines +406 to +410
if let Some(derivation_path) = derivation_path {
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
} else {
keypair_from_seed(seed)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the if clause I mentioned to retain compatibility with current keypair creation. Worth considering if we need to do this. We could transition all creation-from-seed to bip32_derived_keypair with derivation_path of length 0

@codecov
Copy link

codecov bot commented May 1, 2021

Codecov Report

Merging #16942 (0a7bb48) into master (7cea2c4) will decrease coverage by 0.0%.
The diff coverage is 79.7%.

@@            Coverage Diff            @@
##           master   #16942     +/-   ##
=========================================
- Coverage    82.8%    82.8%   -0.1%     
=========================================
  Files         416      416             
  Lines      117032   117189    +157     
=========================================
+ Hits        96928    97047    +119     
- Misses      20104    20142     +38     

Copy link
Contributor

@t-nelson t-nelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just a couple nits so far. Let me know when it's ready for another pass

@CriesofCarrots CriesofCarrots marked this pull request as ready for review May 3, 2021 22:43
@CriesofCarrots
Copy link
Contributor Author

@t-nelson ready for another look!

Copy link
Contributor

@t-nelson t-nelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. Thanks for driving this home!

@CriesofCarrots CriesofCarrots merged commit 694c674 into solana-labs:master May 4, 2021
mergify bot pushed a commit that referenced this pull request May 4, 2021
* Add Keypair helpers for bip32 derivation

* Plumb bip32 for SignerSourceKind::Ask

* Support full-path querystring

* Use as_ref

* Add public wrappers for from_uri cases

* Support master root derivations (and fix too-deep print

* Add ask:// HD documentation

* Update ASK elsewhere in docs

(cherry picked from commit 694c674)

# Conflicts:
#	programs/bpf/Cargo.lock
mergify bot added a commit that referenced this pull request May 4, 2021
…17018)

* Implement Bip32 for seed-phrase/passphrase signing (#16942)

* Add Keypair helpers for bip32 derivation

* Plumb bip32 for SignerSourceKind::Ask

* Support full-path querystring

* Use as_ref

* Add public wrappers for from_uri cases

* Support master root derivations (and fix too-deep print

* Add ask:// HD documentation

* Update ASK elsewhere in docs

(cherry picked from commit 694c674)

# Conflicts:
#	programs/bpf/Cargo.lock

* Fix conflict

Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
Co-authored-by: Tyera Eulberg <tyera@solana.com>

To display the wallet address of a Paper Wallet:

```bash
solana-keygen pubkey ASK
solana-keygen pubkey ask://
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these generate the same address? At a glance, I'd guess that ask:// would generate the key at ASK/44'/501'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, these do generate the same address. Have you read this section yet? https://github.com/solana-labs/solana/pull/16942/files#diff-74038ef7d737ad7ebedf6ddb1f940e6f15337de6cba7d331a3621cccdd81cd3aR123
Definitely would love suggestions to make docs more clear.

Copy link
Contributor

@garious garious May 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, do I have to write ask://?full-path=""? As I recall, usb:// gets the Solana-specific address at 44'/501'. I don't see why ask:// wouldn't do the same (which is different than what ASK would have output).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you would write ask://?key=m to use solana's bip44 derivation path root, 44'/501'.
Now that we're depending on the derivation-path crate, usb://ledger?key=m actually also derives the solana bip44 root, although of course it's easier to just not supply the query string at all.

I can see the argument for making ask:// no query behave the same as usb:// no query, but I was concerned about retaining compatibility for current seed phrase users. They would otherwise have to continue using ASK or we would have to build in a special query string for ask://, since the current output of ASK is a key that is not anywhere in the bip32 derivation.
cc @t-nelson

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to work toward documenting everything in URI form going forward. ask: (I'm somewhat dubious as to whether we should support the network prefix ://) is a direct replacement for ASK, which should be considered deprecated. usb:// is overloaded in that it implicitly derives BIP44 keypairs. This is the bug IMO, but we keep the special-case for backward compatibility. So: ask: gives the raw ed25519 keypair, ask:?key=X[/Y] gives the BIP44 account[/change] address, ask:?full-path=m/X[/Y[/Z[/...]]] gives the corresponding BIP32 address. This is consistent across all other schemes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

  • ask:// means ASK, does not accept a query string, and does not imply any key derivation
  • ask://solana is an alias for ask://?prefix=44'/501'
  • usb://ledger is an alias for usb://ledger?prefix=44'/501'

Also, how about renaming ask:// to seedphrase:// or stdin://seedphrase?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garious I rather like stdin://seedphrase. We do already have a stdin scheme implementation for piping in json keypair (same as passing -). What host do you like for that?

For seedphrase, I would envision:

  • stdin://seedphrase => ASK (raw ed25519 keypair)
  • stdin://seedphrase?key=1/2 => solana bip44 m/44/501/1/2
  • stdin://seedphrase?full-path=m/44/2017/1/2

Or would you want to use the solana alias as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't care for 'full-path' because it conflicts with 'key' (unless 'key' always appends, in which case 'full-path' is not the "full" path). A 'prefix' parameter that defaults to "m/44'/501'" seems more intuitive to me. To get ASK, you can then write "stdin://seedphrase?prefix=''"

jeffwashington pushed a commit to jeffwashington/solana that referenced this pull request May 6, 2021
* Add Keypair helpers for bip32 derivation

* Plumb bip32 for SignerSourceKind::Ask

* Support full-path querystring

* Use as_ref

* Add public wrappers for from_uri cases

* Support master root derivations (and fix too-deep print

* Add ask:// HD documentation

* Update ASK elsewhere in docs
@CriesofCarrots CriesofCarrots deleted the bip32-ask branch May 10, 2021 20:09
@brooksprumo brooksprumo mentioned this pull request Aug 23, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants