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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ jobs:
run: cargo check -p kobe-nostr --target thumbv7m-none-eabi --no-default-features --features alloc
- name: kobe-xrpl (no_std)
run: cargo check -p kobe-xrpl --target thumbv7m-none-eabi --no-default-features --features alloc
- name: kobe-casper (no_std)
run: cargo check -p kobe-casper --target thumbv7m-none-eabi --no-default-features --features alloc
- name: kobe (no_std)
run: cargo check -p kobe --target thumbv7m-none-eabi --no-default-features --features alloc
- name: kobe (no_std + all chains)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ jobs:
publish:
uses: qntx/workflows/.github/workflows/publish-crates.yml@main
with:
packages: "kobe-primitives kobe-aptos kobe-btc kobe-evm kobe-svm kobe-cosmos kobe-tron kobe-spark kobe-fil kobe-ton kobe-sui kobe-xrpl kobe-nostr kobe kobe-cli"
packages: "kobe-primitives kobe-aptos kobe-btc kobe-evm kobe-svm kobe-cosmos kobe-tron kobe-spark kobe-fil kobe-ton kobe-sui kobe-xrpl kobe-nostr kobe-casper kobe kobe-cli"
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this workspace are documented in this file. The format is

## [Unreleased]

### Added

- **Casper Network (`kobe-casper`)** — offline HD derivation for CSPR
(SLIP-44 coin type `506`):
- Default algorithm **secp256k1** at Ledger path `m/44'/506'/0'/0/{i}`
- 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…`)
- CLI: `kobe casper` / `kobe cspr` (`new` / `import`)
- Umbrella feature: `casper` (included in `all-chains`)

## [3.1.1] - 2026-08-08

### Security
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ kobe-tron = { version = "3.1", path = "crates/kobe-tron", default-fe
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 }

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: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ check-no-std:
cargo check -p kobe-sui --no-default-features --features alloc
cargo check -p kobe-nostr --no-default-features --features alloc
cargo check -p kobe-xrpl --no-default-features --features alloc
cargo check -p kobe-casper --no-default-features --features alloc
cargo check -p kobe --no-default-features --features alloc
cargo check -p kobe --no-default-features --features "alloc,all-chains"

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ check-no-std:
cargo check -p kobe-sui --no-default-features --features alloc
cargo check -p kobe-nostr --no-default-features --features alloc
cargo check -p kobe-xrpl --no-default-features --features alloc
cargo check -p kobe-casper --no-default-features --features alloc
cargo check -p kobe --no-default-features --features alloc
cargo check -p kobe --no-default-features --features "alloc,all-chains"

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
[rust-badge]: https://img.shields.io/badge/rust-edition%202024-orange.svg
[rust-url]: https://doc.rust-lang.org/edition-guide/

**`no_std`-compatible Rust toolkit for multi-chain HD wallet derivation — one BIP-39 seed, twelve networks, zero hand-written cryptography, cross-implementation KATs.**
**`no_std`-compatible Rust toolkit for multi-chain HD wallet derivation — one BIP-39 seed, thirteen networks, zero hand-written cryptography, cross-implementation KATs.**

Kobe derives standards-compliant accounts and addresses for Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, and Nostr (NIP-06 / NIP-19) from a single BIP-39 mnemonic. It layers thin wrappers around [`bip39`](https://docs.rs/bip39), [`bip32`](https://docs.rs/bip32), [`k256`](https://docs.rs/k256), and [`ed25519-dalek`](https://docs.rs/ed25519-dalek) on top of a unified `Wallet` + `Derive` trait surface (Bitcoin address/WIF encoding is local in `kobe-btc`, not the full `bitcoin` crate); every library crate builds under `no_std + alloc`, mnemonics and private keys wrap in `Zeroizing<T>` and wipe on drop, and every chain's pipeline is pinned against independent reference implementations (bitcoinjs-lib, @ton/core, @noble/hashes, NIP-06 official vectors, ethanmarcuss/spark-address, …).
Kobe derives standards-compliant accounts and addresses for Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr (NIP-06 / NIP-19), and Casper from a single BIP-39 mnemonic. It layers thin wrappers around [`bip39`](https://docs.rs/bip39), [`bip32`](https://docs.rs/bip32), [`k256`](https://docs.rs/k256), and [`ed25519-dalek`](https://docs.rs/ed25519-dalek) on top of a unified `Wallet` + `Derive` trait surface (Bitcoin address/WIF encoding is local in `kobe-btc`, not the full `bitcoin` crate); every library crate builds under `no_std + alloc`, mnemonics and private keys wrap in `Zeroizing<T>` and wipe on drop, and every chain's pipeline is pinned against independent reference implementations (bitcoinjs-lib, @ton/core, @noble/hashes, NIP-06 official vectors, ethanmarcuss/spark-address, casper-types AccountHash preimage, …).

> **See also** [`signer`](https://github.com/qntx/signer) — the companion transaction-signing toolkit that consumes kobe's derived accounts via `Signer::from_derived`.

Expand Down Expand Up @@ -61,6 +61,8 @@ kobe svm new # Solana (Phantom / Backpack / Solf
kobe cosmos new # Cosmos Hub (`cosmos1…`)
kobe aptos new # Aptos
kobe sui new # Sui
kobe casper new # Casper (CSPR)
kobe casper new --algo ed25519 # Casper Ed25519 path
kobe ton new # TON wallet v5r1 (UQ… non-bounceable)
kobe ton new --bounceable # TON bounceable (EQ…), smart-contract style
kobe ton new --testnet --workchain -1 # TON testnet masterchain
Expand Down Expand Up @@ -91,7 +93,7 @@ kobe upgrade --force # reinstall even when up to date
echo "abandon abandon ... about" | kobe -r evm import -m -
```

Every chain subcommand accepts the shared flags `-w/--words`, `-c/--count`, `-p/--passphrase`, and `--qr` through a flattened `SimpleArgs` group, so ergonomics stay consistent across the 12 networks. Global `-r` / `--reveal` opts into printing mnemonics and private keys (default: hidden).
Every chain subcommand accepts the shared flags `-w/--words`, `-c/--count`, `-p/--passphrase`, and `--qr` through a flattened `SimpleArgs` group, so ergonomics stay consistent across the 13 networks. Global `-r` / `--reveal` opts into printing mnemonics and private keys (default: hidden).

### Library Usage

Expand Down Expand Up @@ -169,13 +171,15 @@ println!("Mnemonic: {}", wallet.mnemonic());
| Sui | `kobe-sui` | Ed25519 (SLIP-10) | 784 | `m/44'/784'/{i}'/0'/0'` | `0x` + hex(`BLAKE2b-256(0x00 ‖ pubkey)`) |
| TON | `kobe-ton` | Ed25519 (SLIP-10) | 607 | `m/44'/607'/{i}'` | wallet v5r1 (`UQ…` / `EQ…` / `0Q…` / …) |
| Aptos | `kobe-aptos` | Ed25519 (SLIP-10) | 637 | `m/44'/637'/{i}'/0'/0'` | `0x` + hex(`SHA3-256(pubkey ‖ 0x00)`) |
| Casper | `kobe-casper` | secp256k1 / Ed25519 | 506 | `m/44'/506'/0'/0/{i}` ‡ | `account-hash-` + BLAKE2b-256 |

\* Cosmos coin type defaults to `118`; Terra (`330`), Secret (`529`), Kava (`459`), and custom chains are selectable via `ChainConfig`.
† Spark purpose `8797555` is Spark-specific (`SHA-256("spark")` truncated), not a BIP-44 assignment.
‡ Casper default is secp256k1 (Ledger); Ed25519 uses `m/44'/506'/0'/0'/{i}'`. AccountHash preimage is `algorithm_name || 0x00 || raw_pubkey` per `casper-types`.

## Design

- **12 chains** — Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr — one BIP-39 seed
- **13 chains** — Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr, Casper — one BIP-39 seed
- **Mature crypto dependencies** — `bip39` for mnemonic ↔ entropy, `bip32` + `k256` for BIP-32 secp256k1 (via `kobe-primitives`), `ed25519-dalek` for SLIP-10 Ed25519; hashing via `sha2` / `sha3` / `blake2` / `ripemd`; encoding via `bech32` / `bs58` (Bitcoin addresses/WIF implemented in-tree and KAT-pinned)
- **Unified derivation contract** — shared `Derive` trait with an associated `Account` type + shared `DerivationStyle` trait; every chain has typed public keys via `DerivedPublicKey`, one shared `DeriveError`, and one shared `ParseDerivationStyleError`
- **Consistent entry points** — `derive` / `derive_with` / `derive_at` / `derive_at_with` across every chain (Bitcoin's structured path also available as `derive_structured`)
Expand Down
9 changes: 8 additions & 1 deletion crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| **[`kobe-spark`](kobe-spark/)** | [![crates.io][kobe-spark-crate]][kobe-spark-crate-url] [![docs.rs][kobe-spark-doc]][kobe-spark-doc-url] | Spark (Bitcoin L2) — identity keys + Bech32m `spark1…` addresses |
| **[`kobe-xrpl`](kobe-xrpl/)** | [![crates.io][kobe-xrpl-crate]][kobe-xrpl-crate-url] [![docs.rs][kobe-xrpl-doc]][kobe-xrpl-doc-url] | XRP Ledger — classic `r`-addresses, secp256k1 |
| **[`kobe-nostr`](kobe-nostr/)** | [![crates.io][kobe-nostr-crate]][kobe-nostr-crate-url] [![docs.rs][kobe-nostr-doc]][kobe-nostr-doc-url] | Nostr — NIP-06 key derivation, NIP-19 bech32 `nsec`/`npub` |
| **[`kobe-casper`](kobe-casper/)** | [![crates.io][kobe-casper-crate]][kobe-casper-crate-url] [![docs.rs][kobe-casper-doc]][kobe-casper-doc-url] | Casper — secp256k1 / Ed25519 HD + AccountHash |
| **[`kobe-cli`](kobe-cli/)** | [![crates.io][kobe-cli-crate]][kobe-cli-crate-url] | CLI — generate, import, derive; `upgrade` via sh.qntx.fun |

## Dependency Graph
Expand All @@ -35,7 +36,8 @@ kobe-cli
├── kobe-svm ── kobe-primitives/slip10
├── kobe-ton ── kobe-primitives/slip10
├── kobe-tron ── kobe-primitives/bip32
└── kobe-xrpl ── kobe-primitives/bip32
├── kobe-xrpl ── kobe-primitives/bip32
└── kobe-casper ── kobe-primitives/bip32 + slip10
```

All chain crates consume key derivation through the wallet-level shortcuts
Expand Down Expand Up @@ -65,6 +67,7 @@ The umbrella `kobe` crate provides fine-grained feature control:
| `aptos` | | Aptos chain support (enables `slip10`) |
| `xrpl` | | XRP Ledger chain support (enables `bip32`) |
| `nostr` | | Nostr chain support (enables `bip32`) |
| `casper` | | Casper Network support (enables `bip32` + `slip10`) |
| `all-chains` | | Enable all chain crates |

[kobe-crate]: https://img.shields.io/crates/v/kobe.svg
Expand Down Expand Up @@ -125,3 +128,7 @@ The umbrella `kobe` crate provides fine-grained feature control:
[kobe-nostr-crate-url]: https://crates.io/crates/kobe-nostr
[kobe-nostr-doc]: https://img.shields.io/docsrs/kobe-nostr.svg
[kobe-nostr-doc-url]: https://docs.rs/kobe-nostr
[kobe-casper-crate]: https://img.shields.io/crates/v/kobe-casper.svg
[kobe-casper-crate-url]: https://crates.io/crates/kobe-casper
[kobe-casper-doc]: https://img.shields.io/docsrs/kobe-casper.svg
[kobe-casper-doc-url]: https://docs.rs/kobe-casper
26 changes: 26 additions & 0 deletions crates/kobe-casper/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "kobe-casper"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
readme = "../../README.md"
description = "Casper Network wallet for Kobe"
keywords = ["casper", "cspr", "wallet", "crypto", "no_std"]
categories = ["cryptography", "no-std"]

[features]
default = ["std"]
std = ["alloc", "kobe-primitives/std", "hex/std"]
alloc = ["kobe-primitives/alloc", "hex/alloc"]

[dependencies]
# Casper uses both secp256k1 (Ledger default path) and Ed25519 (casper-client default).
# Secrets zeroize via `kobe-primitives::DerivedAccount` / key types.
kobe-primitives = { workspace = true, features = ["bip32", "slip10"] }
blake2.workspace = true
hex.workspace = true

[lints]
workspace = true
155 changes: 155 additions & 0 deletions crates/kobe-casper/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//! Casper `PublicKey` tagging and `AccountHash` encoding.
//!
//! Encoding matches `casper-types` (`casper-node` `types` crate):
//!
//! - **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;
use blake2::digest::{Update, VariableOutput};
use kobe_primitives::DeriveError;

/// Prefix applied to the hex-encoded `AccountHash` for display.
pub const ACCOUNT_HASH_PREFIX: &str = "account-hash-";

/// Casper serialization tag for Ed25519 public keys (`PublicKey::Ed25519`).
pub const ED25519_TAG: u8 = 0x01;

/// Casper serialization tag for secp256k1 public keys (`PublicKey::Secp256k1`).
pub const SECP256K1_TAG: u8 = 0x02;

/// Lowercase algorithm name used in the `AccountHash` preimage (Ed25519).
const ED25519_NAME: &[u8] = b"ed25519";

/// Lowercase algorithm name used in the `AccountHash` preimage (secp256k1).
const SECP256K1_NAME: &[u8] = b"secp256k1";

/// Format a 32-byte `AccountHash` digest as `account-hash-` + lowercase hex.
#[inline]
#[must_use]
pub fn format_account_hash(digest: &[u8; 32]) -> String {
format!("{ACCOUNT_HASH_PREFIX}{}", hex::encode(digest))
}

/// Tagged public-key hex (no `0x` prefix): `01 ‖ ed25519` or `02 ‖ secp`.
///
/// `raw_key` must be the 32-byte Ed25519 key or 33-byte compressed secp256k1
/// key (without the Casper algorithm tag).
#[inline]
#[must_use]
pub fn tagged_public_key_hex(tag: u8, raw_key: &[u8]) -> String {
let mut buf = Vec::with_capacity(1 + raw_key.len());
buf.push(tag);
buf.extend_from_slice(raw_key);
hex::encode(buf)
}

/// Compute the Casper `AccountHash` for an Ed25519 public key.
///
/// Preimage: `b"ed25519" || 0x00 || pubkey` (32-byte raw key).
///
/// # Errors
///
/// Returns [`DeriveError::Crypto`] if `BLAKE2b` initialization or finalization
/// fails (should not occur for a fixed 32-byte output size).
pub fn account_hash_ed25519(pubkey: &[u8; 32]) -> Result<[u8; 32], DeriveError> {
account_hash_from_parts(ED25519_NAME, pubkey)
}

/// Compute the Casper `AccountHash` for a compressed secp256k1 public key.
///
/// Preimage: `b"secp256k1" || 0x00 || compressed_pubkey` (33-byte `SEC1`).
///
/// # Errors
///
/// Returns [`DeriveError::Crypto`] if `BLAKE2b` initialization or finalization
/// fails.
pub fn account_hash_secp256k1(compressed_pubkey: &[u8; 33]) -> Result<[u8; 32], DeriveError> {
account_hash_from_parts(SECP256K1_NAME, compressed_pubkey)
}

/// Shared `AccountHash` construction: `name || 0x00 || raw_key` → `BLAKE2b`-256.
fn account_hash_from_parts(algorithm_name: &[u8], raw_key: &[u8]) -> Result<[u8; 32], DeriveError> {
let mut preimage = Vec::with_capacity(algorithm_name.len() + 1 + raw_key.len());
preimage.extend_from_slice(algorithm_name);
preimage.push(0);
preimage.extend_from_slice(raw_key);
blake2b_256(&preimage)
}

/// `BLAKE2b`-256 (empty key), matching Casper's `crypto::blake2b`.
fn blake2b_256(data: &[u8]) -> Result<[u8; 32], DeriveError> {
let mut hasher =
Blake2bVar::new(32).map_err(|e| DeriveError::Crypto(format!("blake2b init: {e}")))?;
hasher.update(data);
let mut out = [0u8; 32];
hasher
.finalize_variable(&mut out)
.map_err(|e| DeriveError::Crypto(format!("blake2b finalize: {e}")))?;
Ok(out)
}

#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "unit tests panic on assertion failure"
)]
mod tests {
use super::*;

/// Independent stdlib-equivalent vectors: preimage is `name||0x00||key`,
/// digest is `BLAKE2b`-256. Cross-checked with Python `hashlib.blake2b`.
#[test]
fn kat_account_hash_ed25519_fixed_key() {
let pk = hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.unwrap();
let pk: [u8; 32] = pk.try_into().unwrap();
let digest = account_hash_ed25519(&pk).unwrap();
assert_eq!(
hex::encode(digest),
"5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
);
assert_eq!(
format_account_hash(&digest),
"account-hash-5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
);
assert_eq!(
tagged_public_key_hex(ED25519_TAG, &pk),
"010123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
);
}

/// secp256k1 generator point (compressed) — independent `BLAKE2b` KAT.
#[test]
fn kat_account_hash_secp256k1_generator() {
let pk = hex::decode("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
.unwrap();
let pk: [u8; 33] = pk.try_into().unwrap();
let digest = account_hash_secp256k1(&pk).unwrap();
assert_eq!(
hex::encode(digest),
"86937931937ee0281e50806b94f8d4993e8869b0689dfa0a21d2946ab677183c"
);
assert_eq!(
tagged_public_key_hex(SECP256K1_TAG, &pk),
"020279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
);
}

/// Preimage must include the null separator; wrong layout must not match.
#[test]
fn preimage_includes_null_separator() {
let pk = [0xab_u8; 32];
let good = account_hash_ed25519(&pk).unwrap();
// Tag-only layout (incorrect for AccountHash) must differ.
let mut wrong = Vec::with_capacity(33);
wrong.push(ED25519_TAG);
wrong.extend_from_slice(&pk);
let wrong_hash = blake2b_256(&wrong).unwrap();
assert_ne!(good, wrong_hash);
}
}
Loading