Skip to content
Closed
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
1 change: 1 addition & 0 deletions stellar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ resolver = "2"

[workspace.dependencies]
soroban-sdk = "22.0.0"
proptest = "1.5.0"
1 change: 1 addition & 0 deletions stellar/stealth-announcer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
proptest = { workspace = true }
41 changes: 34 additions & 7 deletions stellar/stealth-announcer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,39 @@ mod test {
client.announce(&2u32, &addr, &epk, &meta);
let events2 = env.events().all();
let event2 = events2.last().unwrap();
let expected_topics2: soroban_sdk::Vec<Val> = vec![
&env,
symbol_short!("announce").into_val(&env),
2u32.into_val(&env),
addr.into_val(&env),
];
assert_eq!(event2.1, expected_topics2);
assert_eq!(event2.1.get(2).unwrap(), 2u32.into_val(&env));
}

#[cfg(feature = "testutils")]
mod fuzz {
use super::*;
use proptest::prelude::*;

proptest! {
#[test]
fn test_announce_fuzz(
scheme_id in any::<u32>(),
epk_bytes in any::<[u8; 32]>(),
meta_bytes in prop::collection::vec(any::<u8>(), 0..100)
) {
let env = Env::default();
let contract_id = env.register(StealthAnnouncerContract, ());
let client = StealthAnnouncerContractClient::new(&env, &contract_id);

let addr = Address::generate(&env);
let epk = BytesN::from_array(&env, &epk_bytes);
let meta = Bytes::from_slice(&env, &meta_bytes);

client.announce(&scheme_id, &addr, &epk, &meta);

let events = env.events().all();
assert_eq!(events.len(), 1);
let event = events.last().unwrap();

let topics = event.1;
assert_eq!(topics.get(2).unwrap(), scheme_id.into_val(&env));
assert_eq!(topics.get(3).unwrap(), addr.into_val(&env));
}
}
}
}
1 change: 1 addition & 0 deletions stellar/stealth-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
proptest = { workspace = true }
28 changes: 27 additions & 1 deletion stellar/stealth-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,38 @@ mod test {
meta_v1
);

// Update to a new meta-address.
let meta_v2 = Bytes::from_slice(&env, &[2u8; 64]);
client.register_keys(&registrant, &scheme_id, &meta_v2);
assert_eq!(
client.stealth_meta_address_of(&registrant, &scheme_id),
meta_v2
);
}

#[cfg(feature = "testutils")]
mod fuzz {
use super::*;
use proptest::prelude::*;

proptest! {
#[test]
fn test_registry_fuzz(
scheme_id in any::<u32>(),
meta_bytes in any::<[u8; 64]>()
) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(StealthRegistryContract, ());
let client = StealthRegistryContractClient::new(&env, &contract_id);

let registrant = Address::generate(&env);
let meta_address = Bytes::from_slice(&env, &meta_bytes);

client.register_keys(&registrant, &scheme_id, &meta_address).unwrap();

let result = client.stealth_meta_address_of(&registrant, &scheme_id).unwrap();
assert_eq!(result, meta_address);
}
}
}
}
1 change: 1 addition & 0 deletions stellar/wraith-names/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
proptest = { workspace = true }
88 changes: 53 additions & 35 deletions stellar/wraith-names/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,34 +129,30 @@ impl WraithNamesContract {
&env,
&env.crypto().sha256(&entry.stealth_meta_address).to_array(),
);
env.storage()
.instance()
.remove(&DataKey::Reverse(old_meta_hash));
env.storage().instance().remove(&DataKey::Reverse(old_meta_hash));

// Update
let new_entry = NameEntry {
name: name.clone(),
stealth_meta_address: new_meta_address.clone(),
owner,
};
// Update entry
let mut new_entry = entry;
new_entry.stealth_meta_address = new_meta_address.clone();
env.storage().instance().set(&name_key, &new_entry);

// New reverse
// Add new reverse
let new_meta_hash =
BytesN::from_array(&env, &env.crypto().sha256(&new_meta_address).to_array());
env.storage()
.instance()
.set(&DataKey::Reverse(new_meta_hash), &name_hash);

env.events().publish(
(symbol_short!("register"), name_hash),
(symbol_short!("update"), name_hash),
(name, new_meta_address),
);

Ok(())
}

/// Release a name, making it available again.
/// Release a name, making it available for others.
/// Only the current owner can release.
pub fn release(env: Env, owner: Address, name: String) -> Result<(), NamesError> {
owner.require_auth();

Expand All @@ -173,20 +169,17 @@ impl WraithNamesContract {
return Err(NamesError::NotOwner);
}

// Remove reverse
// Remove reverse lookup
let meta_hash = BytesN::from_array(
&env,
&env.crypto().sha256(&entry.stealth_meta_address).to_array(),
);
env.storage()
.instance()
.remove(&DataKey::Reverse(meta_hash));
env.storage().instance().remove(&DataKey::Reverse(meta_hash));

// Remove name
env.storage().instance().remove(&name_key);

env.events()
.publish((symbol_short!("release"), name_hash), name);
env.events().publish((symbol_short!("release"), name_hash), name);

Ok(())
}
Expand All @@ -199,10 +192,11 @@ impl WraithNamesContract {
.instance()
.get(&DataKey::Name(name_hash))
.ok_or(NamesError::NameNotFound)?;

Ok(entry.stealth_meta_address)
}

/// Reverse lookup: find the name for a given stealth meta-address.
/// Reverse lookup: find the name associated with a stealth meta-address.
pub fn name_of(env: Env, stealth_meta_address: Bytes) -> Result<String, NamesError> {
let meta_hash =
BytesN::from_array(&env, &env.crypto().sha256(&stealth_meta_address).to_array());
Expand All @@ -211,42 +205,37 @@ impl WraithNamesContract {
.instance()
.get(&DataKey::Reverse(meta_hash))
.ok_or(NamesError::NameNotFound)?;

let entry: NameEntry = env
.storage()
.instance()
.get(&DataKey::Name(name_hash))
.ok_or(NamesError::NameNotFound)?;

Ok(entry.name)
}

/// Hash a name string to BytesN<32> for use as storage key.
fn hash_name(env: &Env, name: &String) -> BytesN<32> {
let len = name.len() as usize;
let mut buf = [0u8; 32];
if len > 0 {
name.copy_into_slice(&mut buf[..len]);
}
let bytes = Bytes::from_slice(env, &buf[..len]);
BytesN::from_array(env, &env.crypto().sha256(&bytes).to_array())
let mut buf = [0u8; 64];
name.copy_into_slice(&mut buf[..name.len() as usize]);
BytesN::from_array(env, &env.crypto().sha256(&Bytes::from_slice(env, &buf[..name.len() as usize])).to_array())
}

/// Validate name: 3-32 chars, lowercase alphanumeric only.
fn validate_name(_env: &Env, name: &String) -> Result<(), NamesError> {
let len = name.len() as usize;
fn validate_name(env: &Env, name: &String) -> Result<(), NamesError> {
let len = name.len();
if len < 3 {
return Err(NamesError::NameTooShort);
}
if len > 32 {
return Err(NamesError::NameTooLong);
}

// Only allow lowercase alphanumeric
let mut buf = [0u8; 32];
name.copy_into_slice(&mut buf[..len]);
for i in 0..len {
name.copy_into_slice(&mut buf[..len as usize]);
for i in 0..len as usize {
let c = buf[i];
let is_lower = c >= b'a' && c <= b'z';
let is_digit = c >= b'0' && c <= b'9';
if !is_lower && !is_digit {
if !((c >= b'a' && c <= b'z') || (c >= b'0' && c <= b'9')) {
return Err(NamesError::InvalidNameCharacter);
}
}
Expand Down Expand Up @@ -329,6 +318,8 @@ mod test {
let meta = Bytes::from_slice(&env, &[88u8; 64]);

client.register(&owner, &name, &meta);
assert_eq!(client.resolve(&name), meta);

client.release(&owner, &name);

let result = client.try_resolve(&name);
Expand Down Expand Up @@ -360,4 +351,31 @@ mod test {
let result = client.try_register(&owner, &String::from_str(&env, "Alice"), &meta);
assert_eq!(result, Err(Ok(NamesError::InvalidNameCharacter)));
}

#[cfg(feature = "testutils")]
mod fuzz {
use super::*;
use proptest::prelude::*;

proptest! {
#[test]
fn test_names_fuzz(
name_str in "[a-z0-9]{3,32}",
meta_bytes in any::<[u8; 64]>()
) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(WraithNamesContract, ());
let client = WraithNamesContractClient::new(&env, &contract_id);

let owner = Address::generate(&env);
let name = String::from_str(&env, &name_str);
let meta = Bytes::from_slice(&env, &meta_bytes);

client.register(&owner, &name, &meta).unwrap();
assert_eq!(client.resolve(&name), meta);
assert_eq!(client.name_of(&meta), name);
}
}
}
}