Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.bitcoindevkit

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import kotlin.test.assertFailsWith

@RunWith(AndroidJUnit4::class)
class DerivationPathTest {
@Test
fun derivationPathFromString() {
val pathString = "m/84h/0h/0h/0/0"
val expectedPath = "84'/0'/0'/0/0"
val derivationPath = DerivationPath(pathString)
assertEquals(
expected = expectedPath,
actual = derivationPath.toString()
)
}

@Test
fun derivationPathtoString() {
val pathString = "m/44h/1h/0h/1/5"
val expectedPath = "44'/1'/0'/1/5"
val derivationPath = DerivationPath(pathString)
assertEquals(
expected = expectedPath,
actual = derivationPath.toString()
)
}

@Test
fun correctlyIdentifiesMaster() {
val pathString = "m"
val derivationPath = DerivationPath(pathString)
assertTrue(derivationPath.isMaster())

val nonMasterPathString = "m/0h/1/2"
val nonMasterDerivationPath = DerivationPath(nonMasterPathString)
assertFalse(nonMasterDerivationPath.isMaster())

val masterPathString = DerivationPath.master()
assertTrue(masterPathString.isMaster())
}

@Test
fun invalidDerivationPath(){
val invalidPathString = "invalid/path/string"
assertFailsWith<Bip32Exception.InvalidChildNumberFormat> { DerivationPath(invalidPathString) }
}

@Test
fun createChildDerivationPath() {
val parentPathString = "m/44h/0h"
val childIndex = 5u
val expectedNormalChildPath = "44'/0'/5"
val parentDerivationPath = DerivationPath(parentPathString)
val childDerivationPath = parentDerivationPath.child(ChildNumber.Normal(childIndex))

assertEquals(
expected = expectedNormalChildPath,
actual = childDerivationPath.toString()
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think if you set childIndex to 2147483648u and then add assertFailsWith<Exception> { DerivationPath(childDerivationPath.toString()) } it could fail because the path can be constructed but not parsed back because I think we’re bypassing the BIP32 index checks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ahh! that is a dangerous catch! Thanks! I will address this tomorrow

Copy link
Collaborator

Choose a reason for hiding this comment

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

sounds good, overall this pr is looking good though 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Made some updates. Added new test in relation to these changes too checkInvalidNormalChildNumberFails and checkInvalidHardenedChildNumberFails

val expectedHardenedChildPath = "44'/0'/5'"
val hardenedChildDerivationPath = parentDerivationPath.child(ChildNumber.Hardened(childIndex))

assertEquals(
expected = expectedHardenedChildPath,
actual = hardenedChildDerivationPath.toString()
)
}

@Test
fun checkInvalidNormalChildNumberFails(){
val parentPathString = "m/44h/0h"
val aHardenedchildIndex = 2147483648u
val parentDerivationPath = DerivationPath(parentPathString)

assertFailsWith<Bip32Exception.InvalidChildNumber> { parentDerivationPath.child(ChildNumber.Normal(aHardenedchildIndex)) }
}

@Test
fun checkInvalidHardenedChildNumberFails(){
val parentPathString = "m/44h/0h"
val alreadyHardenedIndex = 2147483649u
val parentDerivationPath = DerivationPath(parentPathString)

assertFailsWith<Bip32Exception.InvalidChildNumber> { parentDerivationPath.child(ChildNumber.Hardened(alreadyHardenedIndex)) }
}

@Test
fun extendDerivationPath(){
val basePathString = "m/84h/0h"
val extensions = "0h/1/2"

val expectedExtendedPath = "84'/0'/0'/1/2"

val baseDerivationPath = DerivationPath(basePathString)
val extensionDerivationPath = DerivationPath(extensions)

val extendedDerivationPath = baseDerivationPath.extend(extensionDerivationPath)
assertEquals(
expected = expectedExtendedPath,
actual = extendedDerivationPath.toString()
)
}

@Test
fun conversionToList() {
val pathString = "m/49h/1h/0h/0/10"
val derivationPathFromString = DerivationPath(pathString)

val derivationList = derivationPathFromString.toU32Vec()

// BIP32 standard (0x80000000 or 2147483648) converted to decimal and added to each hardened index:

// 49h = 49 + 2147483648 = 2147483697
// 1h = 1 + 2147483648 = 2147483649
// 0h = 0 + 2147483648 = 2147483648
val expectedListString = "[2147483697, 2147483649, 2147483648, 0, 10]"

assertEquals(
expected = expectedListString,
actual = derivationList.toString()
)
}
}
22 changes: 22 additions & 0 deletions bdk-ffi/src/keys.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::error::{Bip32Error, Bip39Error, DescriptorKeyError};
use crate::types::ChildNumber;
use crate::{impl_from_core_type, impl_into_core_type};

use bdk_wallet::bitcoin::bip32::ChildNumber as BdkChildNumber;
use bdk_wallet::bitcoin::bip32::DerivationPath as BdkDerivationPath;
use bdk_wallet::bitcoin::key::Secp256k1;
use bdk_wallet::bitcoin::secp256k1::rand;
Expand All @@ -15,6 +17,7 @@ use bdk_wallet::keys::{
use bdk_wallet::miniscript::descriptor::{DescriptorXKey, Wildcard};
use bdk_wallet::miniscript::BareCtx;

use std::convert::TryFrom;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::Arc;
Expand Down Expand Up @@ -100,6 +103,25 @@ impl DerivationPath {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}

/// Create a new DerivationPath that is a child of this one.
pub fn child(&self, child_number: ChildNumber) -> Result<Arc<Self>, Bip32Error> {
let validated_child_number = BdkChildNumber::try_from(child_number)?;
Ok(Arc::new(self.0.child(validated_child_number).into()))
}

/// Concatenate `self` with `path` and return the resulting new path.
pub fn extend(&self, other: &DerivationPath) -> Arc<Self> {
let extended_path = self.0.extend(&other.0);
Arc::new(DerivationPath(extended_path))
}

/// Returns the derivation path as a vector of u32 integers.
/// Unhardened elements are copied as is.
/// 0x80000000 is added to the hardened elements.
pub fn to_u32_vec(&self) -> Vec<u32> {
self.0.to_u32_vec()
}
}

impl Display for DerivationPath {
Expand Down
42 changes: 41 additions & 1 deletion bdk-ffi/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ use crate::bitcoin::{
TxOut, Txid,
};
use crate::descriptor::Descriptor;
use crate::error::{CreateTxError, RequestBuilderError};
use crate::error::{Bip32Error, CreateTxError, RequestBuilderError};

use bdk_wallet::bitcoin::absolute::LockTime as BdkLockTime;
use bdk_wallet::chain::spk_client::SyncItem;
use bdk_wallet::chain::BlockId as BdkBlockId;
use bdk_wallet::chain::Merge;

use bdk_wallet::bitcoin::bip32::ChildNumber as BdkChildNumber;
use bdk_wallet::bitcoin::Transaction as BdkTransaction;
use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk_wallet::chain::spk_client::FullScanRequestBuilder as BdkFullScanRequestBuilder;
Expand Down Expand Up @@ -1331,3 +1332,42 @@ impl From<bdk_wallet::TxDetails> for TxDetails {
}
}
}

/// A child number in a derivation path
#[derive(Copy, Clone, uniffi::Enum)]
pub enum ChildNumber {
/// Non-hardened key
Normal {
/// Key index, within [0, 2^31 - 1]
index: u32,
},
/// Hardened key
Hardened {
/// Key index, within [0, 2^31 - 1]
index: u32,
},
}

impl From<BdkChildNumber> for ChildNumber {
fn from(value: BdkChildNumber) -> Self {
match value {
BdkChildNumber::Normal { index } => ChildNumber::Normal { index },
BdkChildNumber::Hardened { index } => ChildNumber::Hardened { index },
}
}
}

impl TryFrom<ChildNumber> for BdkChildNumber {
type Error = Bip32Error;

fn try_from(value: ChildNumber) -> Result<Self, Self::Error> {
match value {
ChildNumber::Normal { index } => {
BdkChildNumber::from_normal_idx(index).map_err(Bip32Error::from)
}
ChildNumber::Hardened { index } => {
BdkChildNumber::from_hardened_idx(index).map_err(Bip32Error::from)
}
}
}
}
Loading