diff --git a/Cargo.lock b/Cargo.lock index cf240a9..048d3d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,21 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.13.0" @@ -615,6 +630,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -638,6 +659,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1211,6 +1238,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1463,6 +1496,7 @@ dependencies = [ "ed25519-dalek", "hex", "octo-crypto", + "proptest", "serde", "sha2", "slip10_ed25519", @@ -1654,6 +1688,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -1674,6 +1727,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -1809,6 +1868,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1995,6 +2063,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.40" @@ -2036,6 +2117,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -2574,6 +2667,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2878,6 +2984,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -2969,6 +3081,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 3514b93..fd3e54a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } rand = "0.8" +proptest = "1" # --- internal crates --- octo-crypto = { path = "crates/crypto" } diff --git a/crates/wallet-core/Cargo.toml b/crates/wallet-core/Cargo.toml index 292ffd8..5e22a14 100644 --- a/crates/wallet-core/Cargo.toml +++ b/crates/wallet-core/Cargo.toml @@ -22,3 +22,4 @@ hex.workspace = true sha2.workspace = true [dev-dependencies] +proptest = { workspace = true } diff --git a/crates/wallet-core/src/derive.rs b/crates/wallet-core/src/derive.rs index 60c85c7..a0cae25 100644 --- a/crates/wallet-core/src/derive.rs +++ b/crates/wallet-core/src/derive.rs @@ -71,6 +71,7 @@ impl WalletSeed { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; use stellar_strkey::ed25519::PublicKey; // Official SEP-0005 Test 1 vector (no passphrase). @@ -122,4 +123,53 @@ mod tests { Err(WalletError::InvalidMnemonic) )); } + + proptest! { + #[test] + fn derivation_is_deterministic_for_any_index( + entropy in any::<[u8; 16]>(), + index in any::() + ) { + let mnemonic = + bip39::Mnemonic::from_entropy(&entropy, bip39::Language::English).unwrap(); + let seed_bytes = bip39::Seed::new(&mnemonic, "").as_bytes().to_vec(); + let seed_a = WalletSeed::from_bytes(seed_bytes.clone()); + let seed_b = WalletSeed::from_bytes(seed_bytes); + let secret_a = seed_a.derive_ed25519_secret(index); + let secret_b = seed_b.derive_ed25519_secret(index); + prop_assert_eq!(*secret_a, *secret_b); + } + + #[test] + fn distinct_indices_yield_distinct_secrets( + entropy in any::<[u8; 16]>(), + index_a in any::(), + index_b in any::() + ) { + prop_assume!(index_a != index_b); + let mnemonic = + bip39::Mnemonic::from_entropy(&entropy, bip39::Language::English).unwrap(); + let seed = + WalletSeed::from_bytes(bip39::Seed::new(&mnemonic, "").as_bytes().to_vec()); + let secret_a = seed.derive_ed25519_secret(index_a); + let secret_b = seed.derive_ed25519_secret(index_b); + prop_assert_ne!(*secret_a, *secret_b); + } + } + + #[test] + fn boundary_indices_derive_without_panic() { + let seed = WalletSeed::from_phrase(VECTOR_MNEMONIC).unwrap(); + // Exercises the hardened-offset OR-mask at the extreme ends of u32: + // 0, 1 (lowest valid indices), HARDENED-1 (highest non-hardened u32 value), + // and u32::MAX (wraps the OR-mask into the already-set upper bit). + for &index in &[0u32, 1, super::HARDENED - 1, u32::MAX] { + let secret = seed.derive_ed25519_secret(index); + let signing = ed25519_dalek::SigningKey::from_bytes(&secret); + let pk = PublicKey(signing.verifying_key().to_bytes()); + let encoded = format!("{pk}"); + let decoded = PublicKey::from_string(&encoded).unwrap(); + assert_eq!(decoded.0, signing.verifying_key().to_bytes()); + } + } } diff --git a/crates/wallet-core/src/signer.rs b/crates/wallet-core/src/signer.rs index fc37801..dccb2c9 100644 --- a/crates/wallet-core/src/signer.rs +++ b/crates/wallet-core/src/signer.rs @@ -590,7 +590,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ) .unwrap(); @@ -631,7 +631,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ) .unwrap(); @@ -658,7 +658,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ) .unwrap(); @@ -682,7 +682,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: "", max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: "", max_base_fee_stroops: 200 }, ), Err(WalletError::InvalidXdr) )); @@ -701,7 +701,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ) .unwrap() .envelope_xdr; @@ -713,7 +713,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &fee_bump_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &fee_bump_xdr, max_base_fee_stroops: 200 }, ), Err(WalletError::InvalidXdr) )); @@ -731,7 +731,7 @@ mod tests { &mainnet_sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ), Err(WalletError::SeedDecryption) )); @@ -750,7 +750,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: max_base_fee, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: max_base_fee }, ) .unwrap(); @@ -776,7 +776,7 @@ mod tests { &sealed, StellarNetwork::Testnet, 0, - &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200, sequence: 0 }, + &FeeBumpRequest { inner_xdr: &inner_xdr, max_base_fee_stroops: 200 }, ); assert!(result.is_ok()); assert_eq!(result.unwrap().source_account, MASTER_ACCOUNT_0);