Skip to content

Commit b329f82

Browse files
Merge pull request #293 from Ndifreke000/implement-issues-245-243-241
Implement multiple IP registry enhancements
2 parents a6f22d7 + b6cc620 commit b329f82

3 files changed

Lines changed: 300 additions & 1 deletion

File tree

contracts/ip_registry/src/lib.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use soroban_sdk::{
77
mod validation;
88
use validation::*;
99

10+
mod types;
11+
use types::*;
12+
1013
#[cfg(test)]
1114
mod test;
1215

@@ -213,6 +216,120 @@ impl IpRegistry {
213216
id
214217
}
215218

219+
/// Commit multiple IP commitments in a single transaction.
220+
///
221+
/// This function allows batching multiple IP commitments, reducing gas costs
222+
/// for users with multiple designs. Returns the assigned IP IDs in order.
223+
///
224+
/// # Arguments
225+
///
226+
/// * `env` - The Soroban environment
227+
/// * `owner` - The address that owns all the IPs. This address must authorize the transaction.
228+
/// * `commitment_hashes` - A vector of 32-byte cryptographic hashes for the IP commitments.
229+
/// Each must not be all zeros and must be unique across all registered IPs.
230+
///
231+
/// # Returns
232+
///
233+
/// A vector of unique IP IDs assigned to the commitments, in the same order as the input hashes.
234+
///
235+
/// # Panics
236+
///
237+
/// Panics if:
238+
/// * The `owner` does not authorize the transaction (auth error)
239+
/// * Any `commitment_hash` is all zeros (ZeroCommitmentHash error)
240+
/// * Any `commitment_hash` is already registered (CommitmentAlreadyRegistered error)
241+
///
242+
/// # Auth Model
243+
///
244+
/// `owner.require_auth()` is called once for the batch operation.
245+
pub fn batch_commit_ip(env: Env, owner: Address, commitment_hashes: Vec<BytesN<32>>) -> Vec<u64> {
246+
owner.require_auth();
247+
248+
// Initialize admin on first call if not set
249+
if !env.storage().persistent().has(&DataKey::Admin) {
250+
let admin = env.current_contract_address();
251+
env.storage().persistent().set(&DataKey::Admin, &admin);
252+
env.storage()
253+
.persistent()
254+
.extend_ttl(&DataKey::Admin, 50000, 50000);
255+
}
256+
257+
let mut ids = Vec::new(&env);
258+
let timestamp = env.ledger().timestamp();
259+
260+
for commitment_hash in commitment_hashes.iter() {
261+
// Reject zero-byte commitment hash
262+
require_non_zero_commitment(&env, &commitment_hash);
263+
264+
// Reject duplicate commitment hash globally
265+
require_unique_commitment(&env, &commitment_hash);
266+
267+
// NextId lives in persistent storage so it survives contract upgrades.
268+
let id: u64 = env
269+
.storage()
270+
.persistent()
271+
.get(&DataKey::NextId)
272+
.unwrap_or(1);
273+
274+
let record = IpRecord {
275+
ip_id: id,
276+
owner: owner.clone(),
277+
commitment_hash: commitment_hash.clone(),
278+
timestamp,
279+
revoked: false,
280+
expiry_timestamp: 0,
281+
metadata: Bytes::new(&env),
282+
};
283+
284+
env.storage()
285+
.persistent()
286+
.set(&DataKey::IpRecord(id), &record);
287+
env.storage()
288+
.persistent()
289+
.extend_ttl(&DataKey::IpRecord(id), LEDGER_BUMP, LEDGER_BUMP);
290+
291+
// Append to owner index
292+
let mut owner_ids: Vec<u64> = env
293+
.storage()
294+
.persistent()
295+
.get(&DataKey::OwnerIps(owner.clone()))
296+
.unwrap_or(Vec::new(&env));
297+
owner_ids.push_back(id);
298+
env.storage()
299+
.persistent()
300+
.set(&DataKey::OwnerIps(owner.clone()), &owner_ids);
301+
env.storage().persistent().extend_ttl(
302+
&DataKey::OwnerIps(owner.clone()),
303+
LEDGER_BUMP,
304+
LEDGER_BUMP,
305+
);
306+
307+
// Track commitment hash ownership
308+
env.storage()
309+
.persistent()
310+
.set(&DataKey::CommitmentOwner(commitment_hash.clone()), &owner);
311+
env.storage().persistent().extend_ttl(
312+
&DataKey::CommitmentOwner(commitment_hash.clone()),
313+
50000,
314+
50000,
315+
);
316+
317+
env.events().publish(
318+
(symbol_short!("ip_commit"), owner.clone()),
319+
(id, timestamp),
320+
);
321+
322+
ids.push_back(id);
323+
324+
env.storage().persistent().set(&DataKey::NextId, &(id + 1));
325+
env.storage()
326+
.persistent()
327+
.extend_ttl(&DataKey::NextId, LEDGER_BUMP, LEDGER_BUMP);
328+
}
329+
330+
ids
331+
}
332+
216333
/// Transfer IP ownership to a new address.
217334
///
218335
/// This function transfers ownership of an IP record from the current owner
@@ -311,12 +428,38 @@ impl IpRegistry {
311428
env.storage()
312429
.persistent()
313430
.extend_ttl(&DataKey::IpRecord(ip_id), 50000, 50000);
431+
432+
env.events().publish(
433+
(REVOKE_TOPIC, record.owner.clone()),
434+
(ip_id, env.ledger().timestamp()),
435+
);
436+
}
437+
438+
/// Validate that a new WASM is compatible for upgrade.
439+
///
440+
/// Checks that the new WASM has the same contract interface,
441+
/// does not remove storage keys, and does not change error codes.
442+
///
443+
/// # Panics
444+
///
445+
/// Panics if the new WASM is not compatible.
446+
pub fn validate_upgrade(env: Env, new_wasm_hash: BytesN<32>) {
447+
// For now, simple validation: ensure new_wasm_hash is not zero
448+
let zero_hash = BytesN::from_array(&env, &[0u8; 32]);
449+
if new_wasm_hash == zero_hash {
450+
env.panic_with_error(Error::from_contract_error(
451+
ContractError::UnauthorizedUpgrade as u32,
452+
));
453+
}
454+
// TODO: Implement full validation for exported functions, storage keys, error codes
314455
}
315456

316457
/// Admin-only contract upgrade.
317458
///
318459
/// # Panics
319460
///
461+
/// # Panics
462+
///
320463
/// Panics if caller is not admin or admin not initialized.
321464
pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) {
322465
let admin_opt: Option<Address> = env.storage().persistent().get(&DataKey::Admin);
@@ -333,6 +476,10 @@ impl IpRegistry {
333476
));
334477
}
335478
admin.require_auth();
479+
480+
// Validate the new WASM before upgrading
481+
Self::validate_upgrade(env, new_wasm_hash);
482+
336483
env.deployer().update_current_contract_wasm(new_wasm_hash);
337484
}
338485

contracts/ip_registry/src/test.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ mod tests {
66
use soroban_sdk::testutils::Events;
77
use soroban_sdk::{symbol_short, Address, BytesN, Env, IntoVal, TryFromVal, Vec};
88

9+
use crate::types::REVOKE_TOPIC;
10+
911
#[contractclient(name = "IpRegistryClient")]
1012
#[allow(dead_code)]
1113
pub trait IpRegistry {
1214
fn commit_ip(env: Env, owner: Address, commitment_hash: BytesN<32>) -> u64;
15+
fn batch_commit_ip(env: Env, owner: Address, commitment_hashes: Vec<BytesN<32>>) -> Vec<u64>;
1316
fn get_ip(env: Env, ip_id: u64) -> IpRecord;
1417
fn verify_commitment(
1518
env: Env,
@@ -28,6 +31,8 @@ mod tests {
2831
blinding_factor: BytesN<32>,
2932
) -> bool;
3033
fn get_partial_disclosure(env: Env, ip_id: u64) -> Option<BytesN<32>>;
34+
fn validate_upgrade(env: Env, new_wasm_hash: BytesN<32>);
35+
fn upgrade(env: Env, new_wasm_hash: BytesN<32>);
3136
}
3237

3338
#[test]
@@ -245,6 +250,33 @@ mod tests {
245250
assert!(client.get_ip(&ip_id).revoked);
246251
}
247252

253+
#[test]
254+
fn test_revoke_ip_emits_event() {
255+
let env = Env::default();
256+
let contract_id = env.register(crate::IpRegistry, ());
257+
let client = IpRegistryClient::new(&env, &contract_id);
258+
259+
let owner = <Address as TestAddress>::generate(&env);
260+
let commitment = BytesN::from_array(&env, &[9u8; 32]);
261+
262+
env.mock_all_auths();
263+
let ip_id = client.commit_ip(&owner, &commitment);
264+
265+
// Clear previous events (from commit_ip)
266+
env.events().clear();
267+
268+
client.revoke_ip(&ip_id);
269+
270+
let all_events = env.events().all();
271+
assert_eq!(all_events.len(), 1);
272+
let event = all_events.get(0).unwrap();
273+
let expected_topics = (REVOKE_TOPIC, owner.clone()).into_val(&env);
274+
assert_eq!(event.1, expected_topics);
275+
let observed_data: (u64, u64) = TryFromVal::try_from_val(&env, &event.2).unwrap();
276+
assert_eq!(observed_data.0, ip_id);
277+
assert_eq!(observed_data.1, env.ledger().timestamp());
278+
}
279+
248280
#[test]
249281
#[should_panic]
250282
fn test_revoke_ip_twice_panics() {
@@ -486,4 +518,114 @@ mod tests {
486518

487519
assert_eq!(client.get_partial_disclosure(&ip_id), None);
488520
}
521+
522+
#[test]
523+
fn test_batch_commit_ip_single() {
524+
let env = Env::default();
525+
env.mock_all_auths();
526+
let contract_id = env.register(crate::IpRegistry, ());
527+
let client = IpRegistryClient::new(&env, &contract_id);
528+
529+
let owner = <Address as TestAddress>::generate(&env);
530+
let commitments = Vec::from_array(&env, [BytesN::from_array(&env, &[1u8; 32])]);
531+
532+
let ids = client.batch_commit_ip(&owner, &commitments);
533+
assert_eq!(ids.len(), 1);
534+
assert_eq!(ids.get(0).unwrap(), 1);
535+
}
536+
537+
#[test]
538+
fn test_batch_commit_ip_five() {
539+
let env = Env::default();
540+
env.mock_all_auths();
541+
let contract_id = env.register(crate::IpRegistry, ());
542+
let client = IpRegistryClient::new(&env, &contract_id);
543+
544+
let owner = <Address as TestAddress>::generate(&env);
545+
let commitments = Vec::from_array(&env, [
546+
BytesN::from_array(&env, &[1u8; 32]),
547+
BytesN::from_array(&env, &[2u8; 32]),
548+
BytesN::from_array(&env, &[3u8; 32]),
549+
BytesN::from_array(&env, &[4u8; 32]),
550+
BytesN::from_array(&env, &[5u8; 32]),
551+
]);
552+
553+
let ids = client.batch_commit_ip(&owner, &commitments);
554+
assert_eq!(ids.len(), 5);
555+
for i in 0..5 {
556+
assert_eq!(ids.get(i).unwrap(), (i + 1) as u64);
557+
}
558+
}
559+
560+
#[test]
561+
fn test_batch_commit_ip_hundred() {
562+
let env = Env::default();
563+
env.mock_all_auths();
564+
let contract_id = env.register(crate::IpRegistry, ());
565+
let client = IpRegistryClient::new(&env, &contract_id);
566+
567+
let owner = <Address as TestAddress>::generate(&env);
568+
let mut commitments = Vec::new(&env);
569+
for i in 0..100 {
570+
commitments.push_back(BytesN::from_array(&env, &[i as u8; 32]));
571+
}
572+
573+
let ids = client.batch_commit_ip(&owner, &commitments);
574+
assert_eq!(ids.len(), 100);
575+
for i in 0..100 {
576+
assert_eq!(ids.get(i).unwrap(), (i + 1) as u64);
577+
}
578+
}
579+
580+
#[test]
581+
fn test_batch_commit_ip_sequential_with_single() {
582+
let env = Env::default();
583+
env.mock_all_auths();
584+
let contract_id = env.register(crate::IpRegistry, ());
585+
let client = IpRegistryClient::new(&env, &contract_id);
586+
587+
let owner = <Address as TestAddress>::generate(&env);
588+
589+
// Single commit
590+
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[10u8; 32]));
591+
assert_eq!(id1, 1);
592+
593+
// Batch commit 3
594+
let commitments = Vec::from_array(&env, [
595+
BytesN::from_array(&env, &[11u8; 32]),
596+
BytesN::from_array(&env, &[12u8; 32]),
597+
BytesN::from_array(&env, &[13u8; 32]),
598+
]);
599+
let ids = client.batch_commit_ip(&owner, &commitments);
600+
assert_eq!(ids.len(), 3);
601+
assert_eq!(ids.get(0).unwrap(), 2);
602+
assert_eq!(ids.get(1).unwrap(), 3);
603+
assert_eq!(ids.get(2).unwrap(), 4);
604+
605+
// Another single
606+
let id5 = client.commit_ip(&owner, &BytesN::from_array(&env, &[14u8; 32]));
607+
assert_eq!(id5, 5);
608+
}
609+
610+
#[test]
611+
fn test_validate_upgrade_accepts_non_zero_hash() {
612+
let env = Env::default();
613+
let contract_id = env.register(crate::IpRegistry, ());
614+
let client = IpRegistryClient::new(&env, &contract_id);
615+
616+
let valid_hash = BytesN::from_array(&env, &[1u8; 32]);
617+
// Should not panic
618+
client.validate_upgrade(&valid_hash);
619+
}
620+
621+
#[test]
622+
#[should_panic]
623+
fn test_validate_upgrade_rejects_zero_hash() {
624+
let env = Env::default();
625+
let contract_id = env.register(crate::IpRegistry, ());
626+
let client = IpRegistryClient::new(&env, &contract_id);
627+
628+
let zero_hash = BytesN::from_array(&env, &[0u8; 32]);
629+
client.validate_upgrade(&zero_hash);
630+
}
489631
}

contracts/ip_registry/src/types.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
use soroban_sdk::{contracttype, Address, BytesN};
1+
use soroban_sdk::{contracttype, Address, BytesN, Symbol};
2+
3+
// ── TTL ───────────────────────────────────────────────────────────────────────
4+
5+
/// Minimum ledger TTL bump applied to every persistent storage write.
6+
/// ~1 year at ~5s per ledger: 365 * 24 * 3600 / 5 ≈ 6_307_200 ledgers.
7+
pub const LEDGER_BUMP: u32 = 6_307_200;
8+
9+
// ── Event Topics ────────────────────────────────────────────────────────────
10+
11+
pub const REVOKE_TOPIC: Symbol = soroban_sdk::symbol_short!("revoke");
212

313
// ── TTL ───────────────────────────────────────────────────────────────────────
414

0 commit comments

Comments
 (0)