Is there a relationship between the secp256k1 public key of the sum of two private keys, and the public keys of those original two private keys?

If I have two secp256k1 private keys and add them together, can I derive the public key for the sum, if I only know the public keys for the two original private keys?

(I think this may be the core of understanding bip32.)

If I have two secp256k1 private keys and add them together, can I derive the public key for the sum, if I only know the public keys for the two original private keys?

Yes.

While the idea of 'public keys' and 'private keys' depends on the cryptosystem (and not the elliptic curve it uses), most ECC-based cryptosystems (including the one used in bitcoin) have a private key that is a secret integer $$a$$, and a public key that is the elliptic curve point $$[a]G$$, where $$G$$ is a publicly known elliptic curve point (which is part of the definition of the curve), and $$[a]G$$ is computed using 'point multiplication'.

So, your question boils down to "if we know two public keys $$[a]G$$ and $$[b]G$$ (with private keys $$a$$ and $$b$$), can I derive the public key $$[a+b]G$$, which is the public key for the sum of the private keys?"

The answer is, yes, because this identity always holds:

$$[a+b]G = [a]G + [b]G$$

where the addition on the left side is integer addition, and the addition on the right side is point addition. The right side is easy to compute, when we can get the public key for the sum of the private keys.

• Thanks, that's a great answer - I've being trying to understand bip32 for weeks and now you've provided what I was missing. Sep 3 '20 at 18:54

Here's a very rough implementation in Rust:

use rand::Rng;
use secp256k1::{Secp256k1, SecretKey, PublicKey};

fn main() {
let secp = Secp256k1::new();

let mut skey_a = SecretKey::from_slice(&seed_a).expect("32 bytes, within curve order");
let pkey_a = PublicKey::from_secret_key(&secp, &skey_a);

println!("skey_a {:?}", skey_a);
println!("pkey_a {:?}", pkey_a);

let skey_b = SecretKey::from_slice(&seed_b).expect("32 bytes, within curve order");
let pkey_b = PublicKey::from_secret_key(&secp, &skey_b);

println!("skey_b {:?}", skey_b);
println!("pkey_b {:?}", pkey_b);

skey_a.add_assign(&seed_b).unwrap(); // there is no plain add, nor an obvious way to get the bytes out of an skey
let skey_sum = skey_a;
let pkey_sum_from_skeys = PublicKey::from_secret_key(&secp, &skey_sum);

println!("skey_sum {:?}", skey_sum);
println!("pkey_sum_from_skeys {:?}", pkey_sum_from_skeys);

// calculate the sum of public keys without needing access to secret keys
let pkey_sum_from_pkeys = pkey_a.combine(&pkey_b).unwrap();

println!("pkey_sum_from_pkeys {:?}", pkey_sum_from_pkeys);
}


A sample output is:

skey_a SecretKey(ce9b44fedaa9aa82ee394a488df5ac55bdd3bd62c8cae45bebc1c91174fac2c2)