use std::collections::BTreeMap;
use bitcoin::{
bip32::{ChildNumber, DerivationPath, Xpriv},
hashes::{Hash, HashEngine, sha256},
secp256k1::{PublicKey, SecretKey},
};
use thiserror::Error;
use uuid::Uuid;
use crate::{secp, types::SwapNetwork};
const PURPOSE: u32 = 84;
#[derive(Debug, Error)]
pub enum KeyringError {
#[error("BIP32 derivation: {0}")]
Bip32(#[from] bitcoin::bip32::Error),
#[error("invalid secp256k1 scalar: {0}")]
Scalar(#[from] bitcoin::secp256k1::Error),
#[error("branch {0} was not declared at construction")]
UnknownBranch(u32),
}
#[derive(Clone)]
pub struct Keyring {
branches: BTreeMap<u32, Xpriv>,
}
impl Keyring {
pub fn from_seed(
seed: &[u8],
network: SwapNetwork,
branches: &[u32],
) -> Result<Self, KeyringError> {
let master = Xpriv::new_master(network.bitcoin_network(), seed)?;
let branches = branches
.iter()
.map(|&branch| Ok((branch, derive_branch(&master, network, branch)?)))
.collect::<Result<_, KeyringError>>()?;
Ok(Self { branches })
}
pub fn swap_key(&self, branch: u32, swap_id: &Uuid) -> Result<SecretKey, KeyringError> {
Ok(derive_swap_key(self.branch(branch)?, swap_id)?)
}
pub fn branch_privkey(&self, branch: u32) -> Result<SecretKey, KeyringError> {
Ok(self.branch(branch)?.private_key)
}
pub fn branch_pubkey(&self, branch: u32) -> Result<PublicKey, KeyringError> {
Ok(self.branch_privkey(branch)?.public_key(secp()))
}
fn branch(&self, branch: u32) -> Result<&Xpriv, KeyringError> {
self.branches
.get(&branch)
.ok_or(KeyringError::UnknownBranch(branch))
}
}
fn derive_branch(
master: &Xpriv,
network: SwapNetwork,
branch: u32,
) -> Result<Xpriv, bitcoin::bip32::Error> {
let coin = match network {
SwapNetwork::Mainnet => 0,
_ => 1,
};
let path = DerivationPath::from(vec![
ChildNumber::from_hardened_idx(PURPOSE).expect("fixed purpose"),
ChildNumber::from_hardened_idx(coin).expect("fixed coin"),
ChildNumber::from_hardened_idx(branch).expect("fixed branch"),
]);
master.derive_priv(secp(), &path)
}
fn derive_swap_key(xpriv: &Xpriv, swap_id: &Uuid) -> Result<SecretKey, bitcoin::secp256k1::Error> {
let mut engine = sha256::Hash::engine();
engine.input(&xpriv.private_key.secret_bytes());
engine.input(swap_id.as_bytes());
let hash = sha256::Hash::from_engine(engine);
SecretKey::from_slice(hash.as_ref())
}
#[cfg(test)]
mod tests {
use super::*;
const SEED: [u8; 32] = [0x42; 32];
const SWAP_ID: &str = "00112233-4455-6677-8899-aabbccddeeff";
const BIP39_TEST_SEED: &str = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4";
fn keyring(network: SwapNetwork) -> Keyring {
Keyring::from_seed(&SEED, network, &[0, 1]).unwrap()
}
fn swap_id() -> Uuid {
Uuid::parse_str(SWAP_ID).unwrap()
}
#[test]
fn keyring_derivation_vectors() {
let swap_id = swap_id();
let cases = [
(
SwapNetwork::Mainnet,
"0f2e2ea115f409ee6e6483e3bd2670fcd75d7d40403f427e267889ee1789493e",
"8b376168533a879dc6e2e0fb30a6e6d05e646732df386971504e40b32ace99f8",
),
(
SwapNetwork::Regtest,
"28734f9f0d768466462ec6c6061cff4246bb18e07048861a153929f534b16bb4",
"1f324e1a744da50a1654784536741afa11ba03020175862082ee9e5a3be716a2",
),
];
for (network, branch0, branch1) in cases {
let keyring = keyring(network);
assert_eq!(
hex::encode(keyring.swap_key(0, &swap_id).unwrap().secret_bytes()),
branch0,
"branch 0 on {network:?}"
);
assert_eq!(
hex::encode(keyring.swap_key(1, &swap_id).unwrap().secret_bytes()),
branch1,
"branch 1 on {network:?}"
);
}
}
#[test]
fn keyring_mainnet_differs_from_test_networks() {
let swap_id = swap_id();
assert_ne!(
keyring(SwapNetwork::Mainnet).swap_key(0, &swap_id).unwrap(),
keyring(SwapNetwork::Regtest).swap_key(0, &swap_id).unwrap()
);
}
#[test]
fn keyring_test_networks_share_derivation() {
let swap_id = swap_id();
let testnet = keyring(SwapNetwork::Testnet);
let signet = keyring(SwapNetwork::Signet);
let regtest = keyring(SwapNetwork::Regtest);
assert_eq!(
testnet.swap_key(0, &swap_id).unwrap(),
signet.swap_key(0, &swap_id).unwrap()
);
assert_eq!(
testnet.swap_key(0, &swap_id).unwrap(),
regtest.swap_key(0, &swap_id).unwrap()
);
}
#[test]
fn keyring_derivation_is_deterministic_and_branch_separated() {
let keyring = keyring(SwapNetwork::Regtest);
let swap_id = Uuid::from_u128(0x1234567890abcdef1234567890abcdef);
assert_eq!(
keyring.swap_key(0, &swap_id).unwrap(),
keyring.swap_key(0, &swap_id).unwrap()
);
assert_ne!(
keyring.swap_key(0, &swap_id).unwrap(),
keyring.swap_key(1, &swap_id).unwrap()
);
assert_ne!(
keyring.swap_key(0, &swap_id).unwrap(),
keyring
.swap_key(0, &Uuid::from_u128(swap_id.as_u128() + 1))
.unwrap()
);
}
#[test]
fn branch_pubkey_matches_branch_privkey() {
let keyring = keyring(SwapNetwork::Regtest);
assert_eq!(
keyring.branch_pubkey(0).unwrap(),
keyring.branch_privkey(0).unwrap().public_key(secp())
);
}
#[test]
fn unregistered_branch_is_reported() {
let keyring = keyring(SwapNetwork::Regtest);
assert!(matches!(
keyring.swap_key(9, &swap_id()),
Err(KeyringError::UnknownBranch(9))
));
assert!(matches!(
keyring.branch_privkey(9),
Err(KeyringError::UnknownBranch(9))
));
assert!(matches!(
keyring.branch_pubkey(9),
Err(KeyringError::UnknownBranch(9))
));
}
#[test]
fn keyring_user_wallet_derivation_vectors() {
let seed = hex::decode(BIP39_TEST_SEED).unwrap();
let swap_id = swap_id();
let mainnet = Keyring::from_seed(&seed, SwapNetwork::Mainnet, &[0, 1, 2, 3]).unwrap();
let signing = mainnet.swap_key(1, &swap_id).unwrap();
assert_eq!(
hex::encode(mainnet.branch_privkey(0).unwrap().secret_bytes()),
"e14f274d16ca0d91031b98b162618061d03930fa381af6d4caf44b01819ab6d4"
);
assert_eq!(
hex::encode(mainnet.branch_pubkey(0).unwrap().serialize()),
"02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b"
);
assert_eq!(
hex::encode(signing.secret_bytes()),
"64aff91bfd58a39f00963638da6c40ddb82052abc62b894845ec5456f3588bf4"
);
assert_eq!(
hex::encode(signing.public_key(secp()).serialize()),
"02dfde19a23be257d8cf6c7ca55c8fee3221e55f2f34453fe8350b02938ba82838"
);
assert_eq!(
hex::encode(mainnet.swap_key(2, &swap_id).unwrap().secret_bytes()),
"c4b95f771db2ab111c22c41f462c87c59cb3bb7363c8ce9e668c02b68840ca37"
);
assert_eq!(
hex::encode(mainnet.swap_key(3, &swap_id).unwrap().secret_bytes()),
"75e7aaa283daca0796ec1b0b18de2bc66ba4b96f6772a7dd2dd672ead8837cdf"
);
let regtest = Keyring::from_seed(&seed, SwapNetwork::Regtest, &[0, 1, 2, 3]).unwrap();
assert_eq!(
hex::encode(regtest.branch_privkey(0).unwrap().secret_bytes()),
"7262788152f6450e0f0b336847e5ed3ea4319e10b793c3a7488a474aa4fbeaae"
);
assert_eq!(
hex::encode(regtest.swap_key(1, &swap_id).unwrap().secret_bytes()),
"396a880b5f9084de3cb2fc641e10739752fd90e196c58f9330069174f7c44a17"
);
assert_eq!(
hex::encode(regtest.swap_key(2, &swap_id).unwrap().secret_bytes()),
"f82050d0a8fc6c72cb88191dd2041ec71719d4af694bb01637f1390a3f511255"
);
assert_eq!(
hex::encode(regtest.swap_key(3, &swap_id).unwrap().secret_bytes()),
"bb5ede6f2bbc0173e9af5b64e4f52d60fbf19c8c119aceb1e89f6a11f460a222"
);
}
}