From b4cb9be4f03fb40b81416d3b01871720d4ebe48e Mon Sep 17 00:00:00 2001 From: "x.qntx.eth" Date: Sat, 8 Aug 2026 15:50:07 +0800 Subject: [PATCH] release: v3.2.0 Ship Casper (kobe-casper) offline HD derivation. Drop CasperAccount public_key_hex shadowing in favor of tagged_public_key_hex; add ed25519 smoke + Debug redaction test. Bump workspace to 3.2.0. --- CHANGELOG.md | 6 ++- Cargo.lock | 32 ++++++------ Cargo.toml | 32 ++++++------ crates/kobe-casper/src/address.rs | 1 - crates/kobe-casper/src/deriver.rs | 67 ++++++++++++++------------ crates/kobe-casper/src/lib.rs | 2 +- crates/kobe/src/lib.rs | 2 +- crates/kobe/tests/cross_chain_smoke.rs | 14 ++++++ 8 files changed, 90 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7a406..11da40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this workspace are documented in this file. The format is ## [Unreleased] +## [3.2.0] - 2026-08-08 + ### Added - **Casper Network (`kobe-casper`)** — offline HD derivation for CSPR @@ -12,9 +14,11 @@ All notable changes to this workspace are documented in this file. The format is - Alternate **Ed25519** (SLIP-10) at `m/44'/506'/0'/0'/{i}'` via `--algo ed25519` - Primary address: Casper **AccountHash** (`account-hash-` + 64 hex); preimage matches `casper-types` (`algorithm_name || 0x00 || raw_pubkey`) - - Library `CasperAccount` also exposes tagged public-key hex (`01…` / `02…`) + - Library `CasperAccount::tagged_public_key_hex` for `01…` / `02…` serialization form + (untagged raw key remains on `DerivedAccount::public_key_hex` via `Deref`) - CLI: `kobe casper` / `kobe cspr` (`new` / `import`) - Umbrella feature: `casper` (included in `all-chains`) + - Cross-chain smoke KATs for default secp and ed25519 abandon@0 ## [3.1.1] - 2026-08-08 diff --git a/Cargo.lock b/Cargo.lock index ef4556e..1ddf747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646,7 +646,7 @@ dependencies = [ [[package]] name = "kobe" -version = "3.1.1" +version = "3.2.0" dependencies = [ "kobe-aptos", "kobe-btc", @@ -666,7 +666,7 @@ dependencies = [ [[package]] name = "kobe-aptos" -version = "3.1.1" +version = "3.2.0" dependencies = [ "hex", "kobe-primitives", @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "kobe-btc" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bech32", "bs58", @@ -689,7 +689,7 @@ dependencies = [ [[package]] name = "kobe-casper" -version = "3.1.1" +version = "3.2.0" dependencies = [ "blake2", "hex", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "kobe-cli" -version = "3.1.1" +version = "3.2.0" dependencies = [ "clap", "colored", @@ -710,7 +710,7 @@ dependencies = [ [[package]] name = "kobe-cosmos" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bech32", "kobe-primitives", @@ -718,7 +718,7 @@ dependencies = [ [[package]] name = "kobe-evm" -version = "3.1.1" +version = "3.2.0" dependencies = [ "alloy-primitives", "kobe-primitives", @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "kobe-fil" -version = "3.1.1" +version = "3.2.0" dependencies = [ "blake2", "kobe-primitives", @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "kobe-nostr" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bech32", "kobe-primitives", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "kobe-primitives" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bip32", "bip39", @@ -760,7 +760,7 @@ dependencies = [ [[package]] name = "kobe-spark" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bech32", "hex", @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "kobe-sui" -version = "3.1.1" +version = "3.2.0" dependencies = [ "blake2", "hex", @@ -779,7 +779,7 @@ dependencies = [ [[package]] name = "kobe-svm" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bs58", "kobe-primitives", @@ -788,7 +788,7 @@ dependencies = [ [[package]] name = "kobe-ton" -version = "3.1.1" +version = "3.2.0" dependencies = [ "base64", "kobe-primitives", @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "kobe-tron" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bs58", "kobe-primitives", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "kobe-xrpl" -version = "3.1.1" +version = "3.2.0" dependencies = [ "bs58", "kobe-primitives", diff --git a/Cargo.toml b/Cargo.toml index a218f46..1d932e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["fuzz"] resolver = "3" [workspace.package] -version = "3.1.1" +version = "3.2.0" edition = "2024" # Minimum supported Rust version. `edition = "2024"` was stabilized in 1.85; # we pin to 1.85 here and enforce it in CI. Bumping this is a breaking change. @@ -19,21 +19,21 @@ keywords = ["crypto", "wallet", "bitcoin", "ethereum", "solana"] categories = ["cryptography::cryptocurrencies"] [workspace.dependencies] -kobe-primitives = { version = "3.1", path = "crates/kobe-primitives", default-features = false } -kobe = { version = "3.1", path = "crates/kobe", default-features = false } -kobe-btc = { version = "3.1", path = "crates/kobe-btc", default-features = false } -kobe-cosmos = { version = "3.1", path = "crates/kobe-cosmos", default-features = false } -kobe-evm = { version = "3.1", path = "crates/kobe-evm", default-features = false } -kobe-fil = { version = "3.1", path = "crates/kobe-fil", default-features = false } -kobe-spark = { version = "3.1", path = "crates/kobe-spark", default-features = false } -kobe-sui = { version = "3.1", path = "crates/kobe-sui", default-features = false } -kobe-svm = { version = "3.1", path = "crates/kobe-svm", default-features = false } -kobe-ton = { version = "3.1", path = "crates/kobe-ton", default-features = false } -kobe-tron = { version = "3.1", path = "crates/kobe-tron", default-features = false } -kobe-aptos = { version = "3.1", path = "crates/kobe-aptos", default-features = false } -kobe-nostr = { version = "3.1", path = "crates/kobe-nostr", default-features = false } -kobe-xrpl = { version = "3.1", path = "crates/kobe-xrpl", default-features = false } -kobe-casper = { version = "3.1", path = "crates/kobe-casper", default-features = false } +kobe-primitives = { version = "3.2", path = "crates/kobe-primitives", default-features = false } +kobe = { version = "3.2", path = "crates/kobe", default-features = false } +kobe-btc = { version = "3.2", path = "crates/kobe-btc", default-features = false } +kobe-cosmos = { version = "3.2", path = "crates/kobe-cosmos", default-features = false } +kobe-evm = { version = "3.2", path = "crates/kobe-evm", default-features = false } +kobe-fil = { version = "3.2", path = "crates/kobe-fil", default-features = false } +kobe-spark = { version = "3.2", path = "crates/kobe-spark", default-features = false } +kobe-sui = { version = "3.2", path = "crates/kobe-sui", default-features = false } +kobe-svm = { version = "3.2", path = "crates/kobe-svm", default-features = false } +kobe-ton = { version = "3.2", path = "crates/kobe-ton", default-features = false } +kobe-tron = { version = "3.2", path = "crates/kobe-tron", default-features = false } +kobe-aptos = { version = "3.2", path = "crates/kobe-aptos", default-features = false } +kobe-nostr = { version = "3.2", path = "crates/kobe-nostr", default-features = false } +kobe-xrpl = { version = "3.2", path = "crates/kobe-xrpl", default-features = false } +kobe-casper = { version = "3.2", path = "crates/kobe-casper", default-features = false } alloy-primitives = { version = "1.6.1", default-features = false, features = ["k256"] } base64 = { version = "0.23.1", default-features = false, features = ["alloc"] } diff --git a/crates/kobe-casper/src/address.rs b/crates/kobe-casper/src/address.rs index e70c5f4..42d86db 100644 --- a/crates/kobe-casper/src/address.rs +++ b/crates/kobe-casper/src/address.rs @@ -5,7 +5,6 @@ //! - **Tagged public key** (serialization / display hex): tag byte + raw key. //! - **`AccountHash`** preimage: `algorithm_name || 0x00 || raw_key` (no tag). -#[cfg(feature = "alloc")] use alloc::{format, string::String, vec::Vec}; use blake2::Blake2bVar; diff --git a/crates/kobe-casper/src/deriver.rs b/crates/kobe-casper/src/deriver.rs index ea50f60..b5323af 100644 --- a/crates/kobe-casper/src/deriver.rs +++ b/crates/kobe-casper/src/deriver.rs @@ -20,13 +20,14 @@ use crate::key_algo::KeyAlgo; /// (`01…` / `02…`) used in serialization contexts. /// /// Implements `Deref` so shared accessors -/// (`address()`, `public_key_bytes()`, `private_key_hex()`, …) work directly. +/// (`address()`, `public_key_bytes()`, `private_key_hex()`, untagged +/// [`DerivedAccount::public_key_hex`], …) work without name shadowing. #[derive(Clone)] pub struct CasperAccount { inner: DerivedAccount, algo: KeyAlgo, /// Lowercase hex of tag ‖ raw public key (no `0x` prefix). - public_key_hex: String, + tagged_public_key_hex: String, } impl core::fmt::Debug for CasperAccount { @@ -34,7 +35,7 @@ impl core::fmt::Debug for CasperAccount { f.debug_struct("CasperAccount") .field("inner", &self.inner) .field("algo", &self.algo) - .field("public_key_hex", &self.public_key_hex) + .field("tagged_public_key_hex", &self.tagged_public_key_hex) .finish() } } @@ -49,24 +50,12 @@ impl CasperAccount { /// Casper tagged public-key hex (`01 ‖ ed25519` or `02 ‖ secp compressed`). /// - /// No `0x` prefix; lowercase. - /// - /// **Name collision note:** this inherent method shadows - /// [`DerivedAccount::public_key_hex`] via `Deref`. Callers that need the - /// untagged raw curve key must use - /// `as_derived_account().public_key_hex()` or `public_key().to_hex()`. - #[inline] - #[must_use] - pub fn public_key_hex(&self) -> &str { - &self.public_key_hex - } - - /// Alias for [`Self::public_key_hex`] — preferred when reading code next - /// to untagged [`DerivedAccount::public_key_hex`]. + /// No `0x` prefix; lowercase. Distinct from untagged + /// [`DerivedAccount::public_key_hex`] (available via `Deref`). #[inline] #[must_use] pub fn tagged_public_key_hex(&self) -> &str { - &self.public_key_hex + &self.tagged_public_key_hex } /// Formatted `AccountHash` (`account-hash-` + 64 hex). Alias for @@ -204,7 +193,7 @@ impl<'a> Deriver<'a> { let compressed = key.compressed_pubkey(); let digest = account_hash_secp256k1(&compressed)?; let address = format_account_hash(&digest); - let public_key_hex = tagged_public_key_hex(SECP256K1_TAG, &compressed); + let tagged = tagged_public_key_hex(SECP256K1_TAG, &compressed); let sk = key.private_key_bytes(); let inner = DerivedAccount::new( @@ -217,7 +206,7 @@ impl<'a> Deriver<'a> { Ok(CasperAccount { inner, algo: KeyAlgo::Secp256k1, - public_key_hex, + tagged_public_key_hex: tagged, }) } @@ -226,7 +215,7 @@ impl<'a> Deriver<'a> { let pubkey_bytes = derived.public_key_bytes(); let digest = account_hash_ed25519(&pubkey_bytes)?; let address = format_account_hash(&digest); - let public_key_hex = tagged_public_key_hex(ED25519_TAG, &pubkey_bytes); + let tagged = tagged_public_key_hex(ED25519_TAG, &pubkey_bytes); let sk_bytes = derived.private_key_bytes(); let inner = DerivedAccount::new( @@ -239,7 +228,7 @@ impl<'a> Deriver<'a> { Ok(CasperAccount { inner, algo: KeyAlgo::Ed25519, - public_key_hex, + tagged_public_key_hex: tagged, }) } } @@ -311,8 +300,11 @@ mod tests { assert_eq!(a.path(), "m/44'/506'/0'/0/0"); assert!(a.address().starts_with("account-hash-")); assert_eq!(a.address().len(), "account-hash-".len() + 64); - assert!(a.public_key_hex().starts_with("02")); - assert_eq!(a.public_key_hex().len(), 2 + 66); // tag + 33-byte key hex + assert!(a.tagged_public_key_hex().starts_with("02")); + assert_eq!(a.tagged_public_key_hex().len(), 2 + 66); // tag + 33-byte key hex + // Untagged raw key via Deref must not equal tagged form. + assert_ne!(a.public_key_hex(), a.tagged_public_key_hex()); + assert_eq!(a.public_key_hex().len(), 66); } /// `AccountHash` recomputed from public key bytes must match `address()`. @@ -325,7 +317,7 @@ mod tests { }; let digest = account_hash_secp256k1(pk).unwrap(); assert_eq!(a.address(), format_account_hash(&digest)); - assert_eq!(a.public_key_hex(), format!("02{}", hex::encode(pk))); + assert_eq!(a.tagged_public_key_hex(), format!("02{}", hex::encode(pk))); } #[test] @@ -340,8 +332,8 @@ mod tests { }; let digest = account_hash_ed25519(pk).unwrap(); assert_eq!(a.address(), format_account_hash(&digest)); - assert!(a.public_key_hex().starts_with("01")); - assert_eq!(a.public_key_hex().len(), 2 + 64); + assert!(a.tagged_public_key_hex().starts_with("01")); + assert_eq!(a.tagged_public_key_hex().len(), 2 + 64); } #[test] @@ -351,7 +343,7 @@ mod tests { let sk = a.private_key_hex(); assert_eq!(sk.as_str(), SECP0_PRIV); assert_eq!(a.address(), SECP0_ADDR); - assert_eq!(a.public_key_hex(), SECP0_TAGGED); + assert_eq!(a.tagged_public_key_hex(), SECP0_TAGGED); } #[test] @@ -363,7 +355,22 @@ mod tests { let sk = a.private_key_hex(); assert_eq!(sk.as_str(), ED0_PRIV); assert_eq!(a.address(), ED0_ADDR); - assert_eq!(a.public_key_hex(), ED0_TAGGED); + assert_eq!(a.tagged_public_key_hex(), ED0_TAGGED); + } + + #[test] + fn debug_redacts_private_key() { + let a = Deriver::new(&test_wallet()).derive(0).unwrap(); + let dbg = format!("{a:?}"); + assert!( + dbg.contains("[REDACTED]"), + "Debug must redact private key: {dbg}" + ); + assert!( + !dbg.contains(SECP0_PRIV), + "Debug must not leak private key hex: {dbg}" + ); + assert!(dbg.contains("tagged_public_key_hex")); } #[test] @@ -385,7 +392,7 @@ mod tests { for (b, s) in batch.iter().zip(single.iter()) { assert_eq!(b.address(), s.address()); assert_eq!(b.path(), s.path()); - assert_eq!(b.public_key_hex(), s.public_key_hex()); + assert_eq!(b.tagged_public_key_hex(), s.tagged_public_key_hex()); } } diff --git a/crates/kobe-casper/src/lib.rs b/crates/kobe-casper/src/lib.rs index 1ab01a9..1851bd7 100644 --- a/crates/kobe-casper/src/lib.rs +++ b/crates/kobe-casper/src/lib.rs @@ -27,7 +27,7 @@ //! //! The algorithm-tagged public-key hex used in Casper serialization / //! CEP-57 contexts (`0x01 ‖ ed25519` or `0x02 ‖ secp compressed`) is -//! exposed separately on [`CasperAccount::public_key_hex`]. +//! exposed separately on [`CasperAccount::tagged_public_key_hex`]. //! //! # Example //! diff --git a/crates/kobe/src/lib.rs b/crates/kobe/src/lib.rs index c73a68e..87a59ce 100644 --- a/crates/kobe/src/lib.rs +++ b/crates/kobe/src/lib.rs @@ -5,7 +5,7 @@ //! //! ```toml //! [dependencies] -//! kobe = { version = "3.1", features = ["evm", "btc", "svm"] } +//! kobe = { version = "3.2", features = ["evm", "btc", "svm"] } //! ``` //! //! ```no_run diff --git a/crates/kobe/tests/cross_chain_smoke.rs b/crates/kobe/tests/cross_chain_smoke.rs index 0044e4f..9fc961b 100644 --- a/crates/kobe/tests/cross_chain_smoke.rs +++ b/crates/kobe/tests/cross_chain_smoke.rs @@ -144,6 +144,20 @@ mod smoke { assert_eq!(a.algo(), kobe::casper::KeyAlgo::Secp256k1); } + #[test] + fn casper_ed25519() { + let w = wallet(); + let a = kobe::casper::Deriver::with_algo(&w, kobe::casper::KeyAlgo::Ed25519) + .derive(0) + .unwrap(); + assert_eq!( + a.address(), + "account-hash-356106f683840956a5bff75d011b236068ceccdf09d5c1a6a748c9355b635e08" + ); + assert_eq!(a.algo(), kobe::casper::KeyAlgo::Ed25519); + assert!(a.tagged_public_key_hex().starts_with("01")); + } + #[test] fn derive_many_agrees() { let w = wallet();