Skip to content
Merged
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 16 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"] }
Expand Down
1 change: 0 additions & 1 deletion crates/kobe-casper/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
67 changes: 37 additions & 30 deletions crates/kobe-casper/src/deriver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ use crate::key_algo::KeyAlgo;
/// (`01…` / `02…`) used in serialization contexts.
///
/// Implements `Deref<Target = DerivedAccount>` 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 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
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()
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -217,7 +206,7 @@ impl<'a> Deriver<'a> {
Ok(CasperAccount {
inner,
algo: KeyAlgo::Secp256k1,
public_key_hex,
tagged_public_key_hex: tagged,
})
}

Expand All @@ -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(
Expand All @@ -239,7 +228,7 @@ impl<'a> Deriver<'a> {
Ok(CasperAccount {
inner,
algo: KeyAlgo::Ed25519,
public_key_hex,
tagged_public_key_hex: tagged,
})
}
}
Expand Down Expand Up @@ -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()`.
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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());
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/kobe-casper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand Down
Loading