Skip to content

Commit fc8d16c

Browse files
feat: add public key hashing support
1 parent efb835a commit fc8d16c

File tree

6 files changed

+151
-1
lines changed

6 files changed

+151
-1
lines changed

crypto/addresses/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl TryFrom<&str> for Prefix {
128128
}
129129

130130
///
131-
/// Kaspa `Address` version (`PubKey`, `PubKey ECDSA`, `ScriptHash`)
131+
/// Kaspa `Address` version (`PubKey`, `PubKey ECDSA`, `ScriptHash`, `PubKeyHash`)
132132
///
133133
/// @category Address
134134
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
@@ -142,6 +142,8 @@ pub enum Version {
142142
PubKeyECDSA = 1,
143143
/// ScriptHash addresses always have the version byte set to 8
144144
ScriptHash = 8,
145+
/// PubKeyHash addresses always have the version byte set to 2 (hashed public key for enhanced privacy)
146+
PubKeyHash = 2,
145147
}
146148

147149
impl TryFrom<&str> for Version {
@@ -152,6 +154,7 @@ impl TryFrom<&str> for Version {
152154
"PubKey" => Ok(Version::PubKey),
153155
"PubKeyECDSA" => Ok(Version::PubKeyECDSA),
154156
"ScriptHash" => Ok(Version::ScriptHash),
157+
"PubKeyHash" => Ok(Version::PubKeyHash),
155158
_ => Err(AddressError::InvalidVersionString(value.to_owned())),
156159
}
157160
}
@@ -163,6 +166,7 @@ impl Version {
163166
Version::PubKey => 32,
164167
Version::PubKeyECDSA => 33,
165168
Version::ScriptHash => 32,
169+
Version::PubKeyHash => 32,
166170
}
167171
}
168172
}
@@ -174,6 +178,7 @@ impl TryFrom<u8> for Version {
174178
match value {
175179
0 => Ok(Version::PubKey),
176180
1 => Ok(Version::PubKeyECDSA),
181+
2 => Ok(Version::PubKeyHash),
177182
8 => Ok(Version::ScriptHash),
178183
_ => Err(AddressError::InvalidVersion(value)),
179184
}
@@ -186,6 +191,7 @@ impl Display for Version {
186191
Version::PubKey => write!(f, "PubKey"),
187192
Version::PubKeyECDSA => write!(f, "PubKeyECDSA"),
188193
Version::ScriptHash => write!(f, "ScriptHash"),
194+
Version::PubKeyHash => write!(f, "PubKeyHash"),
189195
}
190196
}
191197
}

crypto/txscript/src/script_class.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ pub enum ScriptClass {
2828
PubKeyECDSA,
2929
/// Pay to script hash
3030
ScriptHash,
31+
/// Pay to pubkey hash
32+
PubKeyHash,
3133
}
3234

3335
const NON_STANDARD: &str = "nonstandard";
3436
const PUB_KEY: &str = "pubkey";
3537
const PUB_KEY_ECDSA: &str = "pubkeyecdsa";
3638
const SCRIPT_HASH: &str = "scripthash";
39+
const PUB_KEY_HASH: &str = "pubkeyhash";
3740

3841
impl ScriptClass {
3942
pub fn from_script(script_public_key: &ScriptPublicKey) -> Self {
@@ -45,6 +48,8 @@ impl ScriptClass {
4548
Self::PubKeyECDSA
4649
} else if Self::is_pay_to_script_hash(script_public_key_) {
4750
Self::ScriptHash
51+
} else if Self::is_pay_to_pubkey_hash(script_public_key_) {
52+
Self::PubKeyHash
4853
} else {
4954
ScriptClass::NonStandard
5055
}
@@ -81,12 +86,22 @@ impl ScriptClass {
8186
(script_public_key[34] == opcodes::codes::OpEqual)
8287
}
8388

89+
/// Returns true if the script is in the standard
90+
/// pay-to-pubkey-hash (P2PKH) format, false otherwise.
91+
#[inline(always)]
92+
pub fn is_pay_to_pubkey_hash(script_public_key: &[u8]) -> bool {
93+
(script_public_key.len() == 34) && // 2 opcodes number + 32 data
94+
(script_public_key[0] == opcodes::codes::OpData32) &&
95+
(script_public_key[33] == opcodes::codes::OpCheckSig)
96+
}
97+
8498
fn as_str(&self) -> &'static str {
8599
match self {
86100
ScriptClass::NonStandard => NON_STANDARD,
87101
ScriptClass::PubKey => PUB_KEY,
88102
ScriptClass::PubKeyECDSA => PUB_KEY_ECDSA,
89103
ScriptClass::ScriptHash => SCRIPT_HASH,
104+
ScriptClass::PubKeyHash => PUB_KEY_HASH,
90105
}
91106
}
92107

@@ -96,6 +111,7 @@ impl ScriptClass {
96111
ScriptClass::PubKey => MAX_SCRIPT_PUBLIC_KEY_VERSION,
97112
ScriptClass::PubKeyECDSA => MAX_SCRIPT_PUBLIC_KEY_VERSION,
98113
ScriptClass::ScriptHash => MAX_SCRIPT_PUBLIC_KEY_VERSION,
114+
ScriptClass::PubKeyHash => MAX_SCRIPT_PUBLIC_KEY_VERSION,
99115
}
100116
}
101117
}
@@ -115,6 +131,7 @@ impl FromStr for ScriptClass {
115131
PUB_KEY => Ok(ScriptClass::PubKey),
116132
PUB_KEY_ECDSA => Ok(ScriptClass::PubKeyECDSA),
117133
SCRIPT_HASH => Ok(ScriptClass::ScriptHash),
134+
PUB_KEY_HASH => Ok(ScriptClass::PubKeyHash),
118135
_ => Err(Error::InvalidScriptClass(script_class.to_string())),
119136
}
120137
}
@@ -134,6 +151,7 @@ impl From<Version> for ScriptClass {
134151
Version::PubKey => ScriptClass::PubKey,
135152
Version::PubKeyECDSA => ScriptClass::PubKeyECDSA,
136153
Version::ScriptHash => ScriptClass::ScriptHash,
154+
Version::PubKeyHash => ScriptClass::PubKeyHash,
137155
}
138156
}
139157
}

crypto/txscript/src/standard.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,21 @@ fn pay_to_script_hash(script_hash: &[u8]) -> ScriptVec {
3636
SmallVec::from_iter([OpBlake2b, OpData32].iter().copied().chain(script_hash.iter().copied()).chain(once(OpEqual)))
3737
}
3838

39+
/// Creates a new script to pay a transaction output to a hashed pubkey.
40+
/// It is expected that the input is a valid hash of a public key.
41+
fn pay_to_pub_key_hash(pubkey_hash: &[u8]) -> ScriptVec {
42+
// TODO: use ScriptBuilder when add_op and add_data fns or equivalents are available
43+
assert_eq!(pubkey_hash.len(), 32);
44+
SmallVec::from_iter(once(OpData32).chain(pubkey_hash.iter().copied()).chain(once(OpCheckSig)))
45+
}
46+
3947
/// Creates a new script to pay a transaction output to the specified address.
4048
pub fn pay_to_address_script(address: &Address) -> ScriptPublicKey {
4149
let script = match address.version {
4250
Version::PubKey => pay_to_pub_key(address.payload.as_slice()),
4351
Version::PubKeyECDSA => pay_to_pub_key_ecdsa(address.payload.as_slice()),
4452
Version::ScriptHash => pay_to_script_hash(address.payload.as_slice()),
53+
Version::PubKeyHash => pay_to_pub_key_hash(address.payload.as_slice()),
4554
};
4655
ScriptPublicKey::new(ScriptClass::from(address.version).version(), script)
4756
}
@@ -80,6 +89,7 @@ pub fn extract_script_pub_key_address(script_public_key: &ScriptPublicKey, prefi
8089
ScriptClass::PubKey => Ok(Address::new(prefix, Version::PubKey, &script[1..33])),
8190
ScriptClass::PubKeyECDSA => Ok(Address::new(prefix, Version::PubKeyECDSA, &script[1..34])),
8291
ScriptClass::ScriptHash => Ok(Address::new(prefix, Version::ScriptHash, &script[2..34])),
92+
ScriptClass::PubKeyHash => Ok(Address::new(prefix, Version::PubKeyHash, &script[1..33])),
8393
}
8494
}
8595

mining/src/mempool/check_transaction_standard.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ impl Mempool {
186186
}
187187
ScriptClass::PubKey => {}
188188
ScriptClass::PubKeyECDSA => {}
189+
ScriptClass::PubKeyHash => {}
189190
ScriptClass::ScriptHash => {
190191
// todo relax due to on fly calculation
191192
let num_sig_ops = get_sig_op_count_upper_bound::<PopulatedTransaction, SigHashReusedValuesUnsync>(

wallet/core/src/key_hashing.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//!
2+
//! Key hashing utilities for enhanced privacy in UTXO management.
3+
//!
4+
//! This module provides functionality to create addresses from hashed public keys,
5+
//! improving privacy by not exposing the actual public key until the UTXO is spent.
6+
7+
use crate::imports::*;
8+
use kaspa_addresses::{Address, Prefix, Version};
9+
use kaspa_hashes::{Hash, Hasher, TransactionHash};
10+
use secp256k1::PublicKey;
11+
12+
/// Creates a hash of a public key for use in UTXO addresses
13+
pub fn hash_public_key(public_key: &PublicKey) -> Hash {
14+
TransactionHash::hash(&public_key.serialize())
15+
}
16+
17+
/// Creates an address from a hashed public key
18+
pub fn create_hashed_address(public_key: &PublicKey, prefix: Prefix) -> Result<Address> {
19+
let key_hash = hash_public_key(public_key);
20+
let address = Address::new(prefix, Version::PubKeyHash, &key_hash.as_bytes());
21+
Ok(address)
22+
}
23+
24+
/// Creates an address from a hashed public key for Schnorr signatures
25+
pub fn create_hashed_schnorr_address(public_key: &secp256k1::XOnlyPublicKey, prefix: Prefix) -> Result<Address> {
26+
let key_hash = TransactionHash::hash(&public_key.serialize());
27+
let address = Address::new(prefix, Version::PubKeyHash, &key_hash.as_bytes());
28+
Ok(address)
29+
}
30+
31+
/// Derives the next change address using hashed public key
32+
pub fn derive_next_change_address_hashed(public_key: &PublicKey, change_index: u32, prefix: Prefix) -> Result<Address> {
33+
// We'll use a simple derivation by hashing the public key with the index
34+
// In a real implementation, this would use proper HD derivation
35+
let mut data = public_key.serialize().to_vec();
36+
data.extend_from_slice(&change_index.to_le_bytes());
37+
let key_hash = TransactionHash::hash(&data);
38+
let address = Address::new(prefix, Version::PubKeyHash, &key_hash.as_bytes());
39+
Ok(address)
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use super::*;
45+
use secp256k1::Secp256k1;
46+
47+
#[test]
48+
fn test_hash_public_key() {
49+
let secp = Secp256k1::new();
50+
let (_, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
51+
52+
let hash1 = hash_public_key(&public_key);
53+
let hash2 = hash_public_key(&public_key);
54+
55+
// Same public key should produce same hash
56+
assert_eq!(hash1, hash2);
57+
58+
// Hash should be different from the original public key
59+
assert_ne!(hash1.as_bytes(), &public_key.serialize()[..]);
60+
}
61+
#[test]
62+
fn test_create_hashed_address_compare() -> Result<()> {
63+
println!("\n=== Key Hashing for Enhanced Privacy Example ===");
64+
65+
let secp = Secp256k1::new();
66+
let (_secret_key, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
67+
68+
// Create regular address (exposes public key)
69+
let regular_address = Address::new(Prefix::Testnet, Version::PubKeyECDSA, &public_key.serialize());
70+
println!("Regular Address (exposes public key): {}", regular_address);
71+
72+
// Create hashed address (hides public key until spent)
73+
let hashed_address = create_hashed_address(&public_key, Prefix::Testnet)?;
74+
println!("Hashed Address (privacy enhanced): {}", hashed_address);
75+
76+
// Demonstrate that the hashed address doesn't reveal the public key
77+
println!("Public key: {}", faster_hex::hex_string(&public_key.serialize()));
78+
println!("Hashed address payload: {}", faster_hex::hex_string(hashed_address.payload.as_slice()));
79+
80+
// Show that different public keys produce different hashed addresses
81+
let (_, another_public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
82+
let another_hashed_address = create_hashed_address(&another_public_key, Prefix::Testnet)?;
83+
println!("Another hashed address: {}", another_hashed_address);
84+
85+
assert_eq!(another_hashed_address.version, Version::PubKeyHash);
86+
assert_eq!(another_hashed_address.prefix, Prefix::Testnet);
87+
assert_eq!(another_hashed_address.payload.len(), 32);
88+
89+
assert_eq!(hashed_address.version, Version::PubKeyHash);
90+
assert_eq!(hashed_address.prefix, Prefix::Testnet);
91+
assert_eq!(hashed_address.payload.len(), 32);
92+
93+
assert_ne!(hashed_address, another_hashed_address);
94+
println!("✓ Different public keys produce different hashed addresses");
95+
96+
Ok(())
97+
}
98+
99+
#[test]
100+
fn test_derive_next_change_address() {
101+
let secp = Secp256k1::new();
102+
let (_, public_key) = secp.generate_keypair(&mut secp256k1::rand::thread_rng());
103+
104+
let address1 = derive_next_change_address_hashed(&public_key, 0, Prefix::Mainnet).unwrap();
105+
let address2 = derive_next_change_address_hashed(&public_key, 1, Prefix::Mainnet).unwrap();
106+
107+
// Different indices should produce different addresses
108+
assert_ne!(address1, address2);
109+
110+
// Both should be PubKeyHash addresses
111+
assert_eq!(address1.version, Version::PubKeyHash);
112+
assert_eq!(address2.version, Version::PubKeyHash);
113+
}
114+
}

wallet/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub mod error;
8383
pub mod events;
8484
pub mod factory;
8585
mod imports;
86+
pub mod key_hashing;
8687
pub mod message;
8788
pub mod metrics;
8889
pub mod prelude;

0 commit comments

Comments
 (0)