Repository: ergoplatform/sigma-rust (language: rust)

Issue:
BIP-32 path operations for Ledger support
new Extended public key type with methods:
- `new` to construct from result of "0x10 Get extended public key" operation on Ledger;
- `derive` to derive public keys (soft derive only, explained in BIP-32);
- `to_address` returning `Address` (P2PK);

Chain code should be private and not visible to the user;

New type for easy BIP-32 path construction without manual labor with methods:
- `new1` constructor from account # (0, 1, ...)
- `new2` constructor with parameters: account, change(bool), index (check with Yoroi if to use `change` at all);
- `to_bytes` returns bytes to be passed to Ledger in "0x21 Sign transaction" instruction; 


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/error_conversion.rs b/bindings/ergo-lib-wasm/src/error_conversion.rs
index 7f5a0de6b..21b736ceb 100644
--- a/bindings/ergo-lib-wasm/src/error_conversion.rs
+++ b/bindings/ergo-lib-wasm/src/error_conversion.rs
@@ -1,5 +1,6 @@
 //! Trait implementations to simplify conversion of `ergo_lib` errors into `JsValue`s
 
+use std::array::TryFromSliceError;
 use std::num::ParseIntError;
 
 use base16::DecodeError;
@@ -8,6 +9,7 @@ use ergo_lib::ergotree_ir::chain::address::AddressError;
 use ergo_lib::ergotree_ir::chain::digest32::Digest32Error;
 use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValueError;
 use ergo_lib::ergotree_ir::chain::token::TokenAmountError;
+use ergo_lib::wallet::derivation_path::ChildIndexError;
 use ergo_lib::wallet::signing::TxSigningError;
 use ergo_lib::{
     chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError,
@@ -56,6 +58,7 @@ from_error_to_wrap!(TxBuilderError);
 from_error_to_wrap!(TxSigningError);
 from_error_to_wrap!(WalletError);
 from_error_to_wrap!(DecodeError);
+from_error_to_wrap!(TryFromSliceError);
 
 macro_rules! from_error_to_wrap_via_debug {
     ($t:ident) => {
@@ -71,3 +74,4 @@ from_error_to_wrap_via_debug!(ErgoTreeError);
 from_error_to_wrap_via_debug!(ErgoTreeConstantError);
 from_error_to_wrap_via_debug!(ErgoTreeConstantsParsingError);
 from_error_to_wrap_via_debug!(ParseIntError);
+from_error_to_wrap_via_debug!(ChildIndexError);
diff --git a/bindings/ergo-lib-wasm/src/wallet.rs b/bindings/ergo-lib-wasm/src/wallet.rs
index f86754d79..492c4fd8a 100644
--- a/bindings/ergo-lib-wasm/src/wallet.rs
+++ b/bindings/ergo-lib-wasm/src/wallet.rs
@@ -2,6 +2,9 @@
 use ergo_lib::ergotree_ir::chain;
 use wasm_bindgen::prelude::*;
 
+pub mod derivation_path;
+pub mod ext_pub_key;
+
 use crate::{
     box_coll::ErgoBoxes, ergo_state_ctx::ErgoStateContext, error_conversion::to_js,
     secret_key::SecretKeys, transaction::reduced::ReducedTransaction, transaction::Transaction,
diff --git a/bindings/ergo-lib-wasm/src/wallet/derivation_path.rs b/bindings/ergo-lib-wasm/src/wallet/derivation_path.rs
new file mode 100644
index 000000000..e9cbefec0
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/wallet/derivation_path.rs
@@ -0,0 +1,75 @@
+//! Derivation path according to
+//! BIP-44 <https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki>
+//! and EIP-3 <https://github.com/ergoplatform/eips/blob/master/eip-0003.md>
+
+use ergo_lib::wallet::derivation_path::ChildIndexError;
+use ergo_lib::wallet::derivation_path::ChildIndexHardened;
+use ergo_lib::wallet::derivation_path::ChildIndexNormal;
+use ergo_lib::wallet::derivation_path::DerivationPath as InnerDerivationPath;
+use wasm_bindgen::prelude::*;
+
+use crate::error_conversion::to_js;
+
+extern crate derive_more;
+use derive_more::{From, Into};
+
+/// According to
+/// BIP-44 <https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki>
+/// and EIP-3 <https://github.com/ergoplatform/eips/blob/master/eip-0003.md>
+#[wasm_bindgen]
+#[derive(PartialEq, Eq, Debug, Clone, From, Into)]
+pub struct DerivationPath(InnerDerivationPath);
+
+#[wasm_bindgen]
+impl DerivationPath {
+    /// Create derivation path for a given account index (hardened) and address indices
+    /// `m / 44' / 429' / acc' / 0 / address[0] / address[1] / ...`
+    /// or `m / 44' / 429' / acc' / 0` if address indices are empty
+    /// change is always zero according to EIP-3
+    /// acc is expected as a 31-bit value (32th bit should not be set)
+    pub fn new(acc: u32, address_indices: &[u32]) -> Result<DerivationPath, JsValue> {
+        let acc = ChildIndexHardened::from_31_bit(acc).map_err(to_js)?;
+        let address_indices = address_indices
+            .iter()
+            .map(|i| ChildIndexNormal::normal(*i))
+            .collect::<Result<Vec<ChildIndexNormal>, ChildIndexError>>()
+            .map_err(to_js)?;
+        Ok(DerivationPath(InnerDerivationPath::new(
+            acc,
+            address_indices,
+        )))
+    }
+
+    /// For 0x21 Sign Transaction command of Ergo Ledger App Protocol
+    /// P2PK Sign (0x0D) instruction
+    /// Sign calculated TX hash with private key for provided BIP44 path.
+    /// Data:
+    ///
+    /// Field
+    /// Size (B)
+    /// Description
+    ///
+    /// BIP32 path length
+    /// 1
+    /// Value: 0x02-0x0A (2-10). Number of path components
+    ///
+    /// First derivation index
+    /// 4
+    /// Big-endian. Value: 44’
+    ///
+    /// Second derivation index
+    /// 4
+    /// Big-endian. Value: 429’ (Ergo coin id)
+    ///
+    /// Optional Third index
+    /// 4
+    /// Big-endian. Any valid bip44 hardened value.
+    /// ...
+    /// Optional Last index
+    /// 4
+    /// Big-endian. Any valid bip44 value.
+    ///
+    pub fn ledger_bytes(&self) -> Vec<u8> {
+        self.0.ledger_bytes()
+    }
+}
diff --git a/bindings/ergo-lib-wasm/src/wallet/ext_pub_key.rs b/bindings/ergo-lib-wasm/src/wallet/ext_pub_key.rs
new file mode 100644
index 000000000..2208940fe
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/wallet/ext_pub_key.rs
@@ -0,0 +1,52 @@
+//! Extended public key operations according to BIP-32
+
+use std::convert::TryInto;
+
+use ergo_lib::wallet::derivation_path::ChildIndexNormal;
+use ergo_lib::wallet::ext_pub_key::ChainCode;
+use ergo_lib::wallet::ext_pub_key::ExtPubKey as InnerExtPubKey;
+use ergo_lib::wallet::ext_pub_key::PubKeyBytes;
+use wasm_bindgen::prelude::*;
+
+use super::derivation_path::DerivationPath;
+use crate::address::Address;
+use crate::error_conversion::to_js;
+
+extern crate derive_more;
+use derive_more::{From, Into};
+
+/// Extented public key implemented according to BIP-32
+#[wasm_bindgen]
+#[derive(PartialEq, Eq, Debug, Clone, From, Into)]
+pub struct ExtPubKey(InnerExtPubKey);
+
+#[wasm_bindgen]
+impl ExtPubKey {
+    /// Create ExtPubKey from public key bytes (from SEC1 compressed), chain code and derivation
+    /// path
+    pub fn new(
+        public_key_bytes: &[u8],
+        chain_code: &[u8],
+        derivation_path: &DerivationPath,
+    ) -> Result<ExtPubKey, JsValue> {
+        let public_key_bytes: PubKeyBytes = public_key_bytes.try_into().map_err(to_js)?;
+        let chain_code: ChainCode = chain_code.try_into().map_err(to_js)?;
+        Ok(ExtPubKey(
+            InnerExtPubKey::new(public_key_bytes, chain_code, derivation_path.clone().into())
+                .map_err(to_js)?,
+        ))
+    }
+
+    /// Soft derivation of the child public key with a given index
+    /// index is expected to be a 31-bit value(32th bit should not be set)
+    pub fn derive(&self, index: u32) -> Result<ExtPubKey, JsValue> {
+        let index = ChildIndexNormal::normal(index).map_err(to_js)?;
+        Ok(self.0.derive(index).into())
+    }
+
+    /// Create address (P2PK) from this extended public key
+    pub fn to_address(&self) -> Address {
+        let address: ergo_lib::ergotree_ir::chain::address::Address = self.0.clone().into();
+        address.into()
+    }
+}
diff --git a/ergo-lib/Cargo.toml b/ergo-lib/Cargo.toml
index 47ef3fe2c..7322e98d6 100644
--- a/ergo-lib/Cargo.toml
+++ b/ergo-lib/Cargo.toml
@@ -30,6 +30,8 @@ derive_more = "0.99"
 bounded-vec = { version = "^0.5.0" }
 num-bigint = "0.4.0"
 proptest-derive = {version = "0.3.0", optional = true }
+sha2 = { version = "0.9.8" }
+hmac = { version = "0.11.0" }
 
 [dependencies.proptest]
 # wasm support, via https://altsysrq.github.io/proptest-book/proptest/wasm.html
diff --git a/ergo-lib/src/wallet.rs b/ergo-lib/src/wallet.rs
index abff6b773..24ace3c3f 100644
--- a/ergo-lib/src/wallet.rs
+++ b/ergo-lib/src/wallet.rs
@@ -1,6 +1,8 @@
 //! Wallet-related features for Ergo
 
 pub mod box_selector;
+pub mod derivation_path;
+pub mod ext_pub_key;
 pub mod secret_key;
 pub mod signing;
 pub mod tx_builder;
diff --git a/ergo-lib/src/wallet/derivation_path.rs b/ergo-lib/src/wallet/derivation_path.rs
new file mode 100644
index 000000000..d56f67f70
--- /dev/null
+++ b/ergo-lib/src/wallet/derivation_path.rs
@@ -0,0 +1,151 @@
+//! Derivation path according to
+//! BIP-44 <https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki>
+//! and EIP-3 <https://github.com/ergoplatform/eips/blob/master/eip-0003.md>
+
+/// Index for hardened derivation
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub struct ChildIndexHardened(u32);
+
+impl ChildIndexHardened {
+    /// Create new from a 31-bit value (32th bit should not be set)
+    pub fn from_31_bit(i: u32) -> Result<Self, ChildIndexError> {
+        if i & (1 << 31) == 0 {
+            Ok(ChildIndexHardened(i))
+        } else {
+            Err(ChildIndexError::NumberTooLarge(i))
+        }
+    }
+}
+
+/// Index for normal(non-hardened) derivation
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub struct ChildIndexNormal(u32);
+
+impl ChildIndexNormal {
+    /// Create an index for normal (non-hardened) derivation from 31-bit value(32th bit should not be set)
+    pub fn normal(i: u32) -> Result<Self, ChildIndexError> {
+        if i & (1 << 31) == 0 {
+            Ok(ChildIndexNormal(i))
+        } else {
+            Err(ChildIndexError::NumberTooLarge(i))
+        }
+    }
+}
+
+/// Child index for derivation
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum ChildIndex {
+    /// Index for hardened derivation
+    Hardened(ChildIndexHardened),
+    /// Index for normal(non-hardened) derivation
+    Normal(ChildIndexNormal),
+}
+
+const PURPOSE: ChildIndex = ChildIndex::Hardened(ChildIndexHardened(44));
+const ERG: ChildIndex = ChildIndex::Hardened(ChildIndexHardened(429));
+/// According to EIP-3 change is always 0 (external address)
+const CHANGE: ChildIndex = ChildIndex::Normal(ChildIndexNormal(0));
+
+/// Child index related errors
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ChildIndexError {
+    /// Nomber is too large
+    NumberTooLarge(u32),
+}
+
+impl ChildIndex {
+    /// Create an index for normal (non-hardened) derivation from 31-bit value(32th bit should not be set)
+    pub fn normal(i: u32) -> Result<Self, ChildIndexError> {
+        Ok(ChildIndex::Normal(ChildIndexNormal::normal(i)?))
+    }
+
+    /// Create an index for hardened derivation from 31-bit value(32th bit should not be set)
+    pub fn hardened(i: u32) -> Result<Self, ChildIndexError> {
+        Ok(ChildIndex::Hardened(ChildIndexHardened::from_31_bit(i)?))
+    }
+
+    /// Return 32-bit representation with highest bit set for hard derivation and clear for normal
+    /// derivation
+    pub fn to_bits(&self) -> u32 {
+        match self {
+            ChildIndex::Hardened(index) => (1 << 31) | index.0,
+            ChildIndex::Normal(index) => index.0,
+        }
+    }
+}
+
+/// According to
+/// BIP-44 <https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki>
+/// and EIP-3 <https://github.com/ergoplatform/eips/blob/master/eip-0003.md>
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct DerivationPath(Box<[ChildIndex]>);
+
+impl DerivationPath {
+    /// Create derivation path for a given account index (hardened) and address indices
+    /// `m / 44' / 429' / acc' / 0 / address[0] / address[1] / ...`
+    /// or `m / 44' / 429' / acc' / 0` if address indices are empty
+    /// change is always zero according to EIP-3
+    pub fn new(acc: ChildIndexHardened, address_indices: Vec<ChildIndexNormal>) -> Self {
+        let mut res = vec![PURPOSE, ERG, ChildIndex::Hardened(acc), CHANGE];
+        res.append(
+            address_indices
+                .into_iter()
+                .map(ChildIndex::Normal)
+                .collect::<Vec<ChildIndex>>()
+                .as_mut(),
+        );
+        Self(res.into_boxed_slice())
+    }
+
+    /// For 0x21 Sign Transaction command of Ergo Ledger App Protocol
+    /// P2PK Sign (0x0D) instruction
+    /// Sign calculated TX hash with private key for provided BIP44 path.
+    /// Data:
+    ///
+    /// Field
+    /// Size (B)
+    /// Description
+    ///
+    /// BIP32 path length
+    /// 1
+    /// Value: 0x02-0x0A (2-10). Number of path components
+    ///
+    /// First derivation index
+    /// 4
+    /// Big-endian. Value: 44’
+    ///
+    /// Second derivation index
+    /// 4
+    /// Big-endian. Value: 429’ (Ergo coin id)
+    ///
+    /// Optional Third index
+    /// 4
+    /// Big-endian. Any valid bip44 hardened value.
+    /// ...
+    /// Optional Last index
+    /// 4
+    /// Big-endian. Any valid bip44 value.
+    ///
+    pub fn ledger_bytes(&self) -> Vec<u8> {
+        let mut res = vec![self.0.len() as u8];
+        self.0
+            .iter()
+            .for_each(|i| res.append(&mut i.to_bits().to_be_bytes().to_vec()));
+        res
+    }
+
+    /// Extend the path with the given index.
+    /// Returns this derivation path with added index.
+    pub fn extend(&self, index: ChildIndexNormal) -> DerivationPath {
+        let mut res = self.0.to_vec();
+        res.push(ChildIndex::Normal(index));
+        DerivationPath(res.into_boxed_slice())
+    }
+}
+
+impl ChildIndexNormal {
+    /// Return next index value (incremented)
+    pub fn next(&self) -> ChildIndexNormal {
+        ChildIndexNormal(self.0 + 1)
+    }
+}
diff --git a/ergo-lib/src/wallet/ext_pub_key.rs b/ergo-lib/src/wallet/ext_pub_key.rs
new file mode 100644
index 000000000..84634f6cf
--- /dev/null
+++ b/ergo-lib/src/wallet/ext_pub_key.rs
@@ -0,0 +1,207 @@
+//! Extended public key operations according to BIP-32
+use std::convert::TryInto;
+
+use ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
+use ergotree_ir::chain::address::Address;
+use ergotree_ir::serialization::SigmaParsingError;
+use ergotree_ir::serialization::SigmaSerializable;
+use ergotree_ir::sigma_protocol::dlog_group;
+use ergotree_ir::sigma_protocol::dlog_group::EcPoint;
+use hmac::{Hmac, Mac, NewMac};
+use sha2::Sha512;
+
+use super::derivation_path::ChildIndex;
+use super::derivation_path::ChildIndexNormal;
+use super::derivation_path::DerivationPath;
+
+/// Public key (serialized EcPoint) bytes
+pub type PubKeyBytes = [u8; EcPoint::GROUP_SIZE];
+/// Chain code bytes
+pub type ChainCode = [u8; 32];
+
+type HmacSha512 = Hmac<Sha512>;
+
+/// Extented public key
+/// implemented according to BIP-32
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ExtPubKey {
+    /// Parsed public key (EcPoint)
+    pub public_key: EcPoint,
+    chain_code: ChainCode,
+    /// Derivation path for this extended public key
+    pub derivation_path: DerivationPath,
+}
+
+impl ExtPubKey {
+    /// Create ExtPubKey from public key bytes (from SEC1 compressed), chain code and derivation
+    /// path
+    pub fn new(
+        public_key_bytes: PubKeyBytes,
+        chain_code: ChainCode,
+        derivation_path: DerivationPath,
+    ) -> Result<Self, SigmaParsingError> {
+        let public_key = EcPoint::sigma_parse_bytes(&public_key_bytes)?;
+        Ok(Self {
+            public_key,
+            chain_code,
+            derivation_path,
+        })
+    }
+
+    fn pub_key_bytes(&self) -> PubKeyBytes {
+        self.public_key
+            .sigma_serialize_bytes()
+            .unwrap()
+            .as_slice()
+            .try_into()
+            .unwrap()
+    }
+
+    /// Soft derivation of the child public key with a given index
+    pub fn derive(&self, index: ChildIndexNormal) -> Self {
+        let mut mac = HmacSha512::new_from_slice(&self.chain_code).unwrap();
+        mac.update(&self.pub_key_bytes());
+        mac.update(
+            ChildIndex::Normal(index.clone())
+                .to_bits()
+                .to_be_bytes()
+                .as_ref(),
+        );
+        let mac_bytes = mac.finalize().into_bytes();
+        let mut secret_key_bytes = [0; 32];
+        secret_key_bytes.copy_from_slice(&mac_bytes[..32]);
+        if let Some(child_secret_key) = DlogProverInput::from_bytes(&secret_key_bytes) {
+            let mut chain_code = [0; 32];
+            chain_code.copy_from_slice(&mac_bytes[32..]);
+            let child_pub_key = *child_secret_key.public_image().h * &self.public_key;
+            if dlog_group::is_identity(&child_pub_key) {
+                // point is infinity element, thus repeat with next index value (see BIP-32)
+                self.derive(index.next())
+            } else {
+                ExtPubKey {
+                    public_key: child_pub_key,
+                    chain_code,
+                    derivation_path: self.derivation_path.extend(index),
+                }
+            }
+        } else {
+            // not in range [0, modulus), thus repeat with next index value (BIP-32)
+            self.derive(index.next())
+        }
+    }
+}
+
+impl From<ExtPubKey> for Address {
+    fn from(epk: ExtPubKey) -> Self {
+        Address::P2Pk(epk.public_key.into())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::wallet::derivation_path::ChildIndexHardened;
+
+    use super::*;
+
+    #[test]
+    fn bip32_test_vector0() {
+        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
+        // Chain m/0' from Test vector 1
+        // The difference between path "m/0'" and our "m/44'/429'/0" does not matter
+        // since we only testing soft derivation for children
+        let derivation_path =
+            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
+        let pub_key_bytes =
+            base16::decode(b"035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56")
+                .unwrap();
+        let chain_code =
+            base16::decode(b"47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141")
+                .unwrap();
+        let ext_pub_key = ExtPubKey::new(
+            pub_key_bytes.try_into().unwrap(),
+            chain_code.try_into().unwrap(),
+            derivation_path,
+        )
+        .unwrap();
+
+        // Chain m/0'/1
+        let child = ext_pub_key.derive(ChildIndexNormal::normal(1).unwrap());
+        let expected_child_pub_key_bytes: PubKeyBytes =
+            base16::decode(b"03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c")
+                .unwrap()
+                .try_into()
+                .unwrap();
+        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
+    }
+
+    #[test]
+    fn bip32_test_vector1() {
+        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
+        // Chain m/0'/1/2' from Test vector 1
+        // The difference between path "m/0'/1/2'" and our "m/44'/429'/0" does not matter
+        // since we only testing soft derivation for children
+        let derivation_path =
+            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
+        let pub_key_bytes =
+            base16::decode(b"0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2")
+                .unwrap();
+        let chain_code =
+            base16::decode(b"04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f")
+                .unwrap();
+        let ext_pub_key = ExtPubKey::new(
+            pub_key_bytes.try_into().unwrap(),
+            chain_code.try_into().unwrap(),
+            derivation_path,
+        )
+        .unwrap();
+
+        // Chain m/0'/1/2'/2
+        let child = ext_pub_key.derive(ChildIndexNormal::normal(2).unwrap());
+        let expected_child_pub_key_bytes: PubKeyBytes =
+            base16::decode(b"02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29")
+                .unwrap()
+                .try_into()
+                .unwrap();
+        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
+
+        // Chain m/0'/1/2'/2/1000000000
+        let child2 = child.derive(ChildIndexNormal::normal(1000000000).unwrap());
+        let expected_child2_pub_key_bytes: PubKeyBytes =
+            base16::decode(b"022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011")
+                .unwrap()
+                .try_into()
+                .unwrap();
+        assert_eq!(child2.pub_key_bytes(), expected_child2_pub_key_bytes);
+    }
+
+    #[test]
+    fn bip32_test_vector2() {
+        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
+        // Chain m from Test vector 2
+        // The difference between path "m" and our "m/44'/429'/0" does not matter
+        // since we only testing soft derivation for children
+        let derivation_path =
+            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
+        let pub_key_bytes =
+            base16::decode(b"03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7")
+                .unwrap();
+        let chain_code =
+            base16::decode(b"60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
+                .unwrap();
+        let ext_pub_key = ExtPubKey::new(
+            pub_key_bytes.try_into().unwrap(),
+            chain_code.try_into().unwrap(),
+            derivation_path,
+        )
+        .unwrap();
+
+        // Chain m/0
+        let child = ext_pub_key.derive(ChildIndexNormal::normal(0).unwrap());
+        let expected_child_pub_key_bytes: PubKeyBytes =
+            base16::decode(b"02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea")
+                .unwrap()
+                .try_into()
+                .unwrap();
+        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
+    }
+}


Test patch: