Skip to content

Commit 657f4e3

Browse files
Merge pull request #386 from mgtmdccix-oss/feat/pow-difficulty-commit-ip
feat: add proof-of-work requirement to commit_ip
2 parents 9910f0c + afc0fca commit 657f4e3

3 files changed

Lines changed: 163 additions & 33 deletions

File tree

contracts/ip_registry/src/lib.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub enum ContractError {
2828
IpExpired = 7,
2929
MetadataTooLarge = 8,
3030
LicenseeNotFound = 9,
31+
InsufficientPoW = 10,
3132
}
3233

3334
// ── TTL ───────────────────────────────────────────────────────────────────────
@@ -52,6 +53,7 @@ pub enum DataKey {
5253
PartialDisclosure(u64), // stores partial_hash for a given ip_id after reveal
5354
IpLicenses(u64), // stores license entries for a given ip_id
5455
CategoryIps(BytesN<32>), // maps category hash -> Vec<u64> of IP IDs
56+
PowDifficulty, // stores the current PoW difficulty (leading zero bits required)
5557
}
5658

5759
// ── Types ────────────────────────────────────────────────────────────────────
@@ -123,7 +125,7 @@ impl IpRegistry {
123125
/// Therefore: a caller cannot forge `owner` in production. They can only
124126
/// commit IP under an address for which they hold a valid private key or
125127
/// delegated authorization.
126-
pub fn commit_ip(env: Env, owner: Address, commitment_hash: BytesN<32>) -> u64 {
128+
pub fn commit_ip(env: Env, owner: Address, commitment_hash: BytesN<32>, pow_difficulty: u32) -> u64 {
127129
// Enforced by the Soroban host: panics if the transaction does not carry
128130
// a valid authorization for `owner`. This is the correct auth pattern.
129131
owner.require_auth();
@@ -143,6 +145,9 @@ impl IpRegistry {
143145
// Reject duplicate commitment hash globally
144146
require_unique_commitment(&env, &commitment_hash);
145147

148+
// Validate proof-of-work: commitment_hash must have `pow_difficulty` leading zero bits
149+
require_pow(&env, &commitment_hash, pow_difficulty);
150+
146151
// NextId lives in persistent storage so it survives contract upgrades.
147152
// Instance storage is wiped on upgrade, which would reset the counter
148153
// and cause ID collisions with existing IP records.
@@ -591,6 +596,15 @@ impl IpRegistry {
591596
.unwrap_or(Vec::new(&env))
592597
}
593598

599+
/// Returns the current PoW difficulty (number of leading zero bits required in commitment_hash).
600+
/// Defaults to 4 if not explicitly set.
601+
pub fn get_pow_difficulty(env: Env) -> u32 {
602+
env.storage()
603+
.persistent()
604+
.get(&DataKey::PowDifficulty)
605+
.unwrap_or(4u32)
606+
}
607+
594608
/// Partially disclose an IP commitment by revealing a hash of the design
595609
/// without exposing the full secret.
596610
///
@@ -854,15 +868,15 @@ mod tests {
854868
invoke: &soroban_sdk::testutils::MockAuthInvoke {
855869
contract: &contract_id,
856870
fn_name: "commit_ip",
857-
args: (bob.clone(), hash.clone()).into_val(&env),
871+
args: (bob.clone(), hash.clone(), 0u32).into_val(&env),
858872
sub_invokes: &[],
859873
},
860874
}]);
861875

862876
// This call passes bob's address as owner but only alice's auth is mocked.
863877
// The SDK MUST reject this with an auth panic — confirming the bug condition
864878
// is correctly enforced at the protocol level.
865-
client.commit_ip(&bob, &hash);
879+
client.commit_ip(&bob, &hash, &0u32);
866880
}
867881

868882
/// Attack Surface Documentation Test — mock_all_auths variant
@@ -891,7 +905,7 @@ mod tests {
891905
// This documents the attack surface: in test environments with relaxed
892906
// auth, a non-owner can register IP under an arbitrary address.
893907
// Counterexample: (invoker=alice, owner=bob) — isBugCondition is true.
894-
let ip_id = client.commit_ip(&bob, &hash);
908+
let ip_id = client.commit_ip(&bob, &hash, &0u32);
895909

896910
// The record is stored under bob, not alice — confirming the forgery.
897911
let record = client.get_ip(&ip_id);
@@ -911,7 +925,7 @@ mod tests {
911925
env.mock_all_auths();
912926

913927
let recorded_time = env.ledger().timestamp();
914-
let ip_id = client.commit_ip(&owner, &commitment);
928+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
915929
let record = client.get_ip(&ip_id);
916930

917931
assert_eq!(record.timestamp, recorded_time);

contracts/ip_registry/src/test.rs

Lines changed: 117 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod tests {
1111
#[contractclient(name = "IpRegistryClient")]
1212
#[allow(dead_code)]
1313
pub trait IpRegistry {
14-
fn commit_ip(env: Env, owner: Address, commitment_hash: BytesN<32>) -> u64;
14+
fn commit_ip(env: Env, owner: Address, commitment_hash: BytesN<32>, pow_difficulty: u32) -> u64;
1515
fn batch_commit_ip(env: Env, owner: Address, commitment_hashes: Vec<BytesN<32>>) -> Vec<u64>;
1616
fn get_ip(env: Env, ip_id: u64) -> IpRecord;
1717
fn verify_commitment(
@@ -33,6 +33,7 @@ mod tests {
3333
fn get_partial_disclosure(env: Env, ip_id: u64) -> Option<BytesN<32>>;
3434
fn validate_upgrade(env: Env, new_wasm_hash: BytesN<32>);
3535
fn upgrade(env: Env, new_wasm_hash: BytesN<32>);
36+
fn get_pow_difficulty(env: Env) -> u32;
3637
}
3738

3839
#[test]
@@ -52,9 +53,9 @@ mod tests {
5253

5354
// Call commit_ip three times with proper authentication
5455
env.mock_all_auths();
55-
let id1 = client.commit_ip(&owner1, &commitment1);
56-
let id2 = client.commit_ip(&owner2, &commitment2);
57-
let id3 = client.commit_ip(&owner1, &commitment3);
56+
let id1 = client.commit_ip(&owner1, &commitment1, &0u32);
57+
let id2 = client.commit_ip(&owner2, &commitment2, &0u32);
58+
let id3 = client.commit_ip(&owner1, &commitment3, &0u32);
5859

5960
// Assert IDs are sequential: 1, 2, 3 (first ID is 1, not 0)
6061
assert_eq!(id1, 1, "First commit should return ID 1");
@@ -98,7 +99,7 @@ mod tests {
9899
env.mock_all_auths();
99100

100101
// Call commit_ip which should emit an event
101-
let ip_id = client.commit_ip(&owner, &commitment);
102+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
102103

103104
// Check events immediately after commit_ip, before any other calls.
104105
let all_events = env.events().all();
@@ -129,7 +130,7 @@ mod tests {
129130

130131
// All-zero hash has no cryptographic value — must panic with ContractError::ZeroCommitmentHash (code 2)
131132
let zero_hash = BytesN::from_array(&env, &[0u8; 32]);
132-
client.commit_ip(&owner, &zero_hash);
133+
client.commit_ip(&owner, &zero_hash, &0u32);
133134
}
134135

135136
#[test]
@@ -154,7 +155,7 @@ mod tests {
154155
let commitment = BytesN::from_array(&env, &[5u8; 32]);
155156

156157
env.mock_all_auths();
157-
let ip_id = client.commit_ip(&alice, &commitment);
158+
let ip_id = client.commit_ip(&alice, &commitment, &0u32);
158159

159160
client.transfer_ip(&ip_id, &bob);
160161

@@ -183,7 +184,7 @@ mod tests {
183184
let commitment = BytesN::from_array(&env, &[6u8; 32]);
184185

185186
env.mock_all_auths();
186-
let ip_id = client.commit_ip(&alice, &commitment);
187+
let ip_id = client.commit_ip(&alice, &commitment, &0u32);
187188

188189
// Only mock bob's auth — alice's auth is not present, so transfer must panic
189190
env.mock_auths(&[soroban_sdk::testutils::MockAuth {
@@ -222,7 +223,7 @@ mod tests {
222223

223224
// Commit an IP for owner
224225
let commitment = BytesN::from_array(&env, &[1u8; 32]);
225-
let ip_id = client.commit_ip(&owner, &commitment);
226+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
226227

227228
// Unknown owner returns empty Vec; known owner returns Vec with IPs.
228229
let unknown_ips = client.list_ip_by_owner(&unknown_owner);
@@ -243,7 +244,7 @@ mod tests {
243244
let commitment = BytesN::from_array(&env, &[7u8; 32]);
244245

245246
env.mock_all_auths();
246-
let ip_id = client.commit_ip(&owner, &commitment);
247+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
247248

248249
assert!(!client.get_ip(&ip_id).revoked);
249250
client.revoke_ip(&ip_id);
@@ -260,7 +261,7 @@ mod tests {
260261
let commitment = BytesN::from_array(&env, &[9u8; 32]);
261262

262263
env.mock_all_auths();
263-
let ip_id = client.commit_ip(&owner, &commitment);
264+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
264265

265266
// Clear previous events (from commit_ip)
266267
env.events().clear();
@@ -286,7 +287,7 @@ mod tests {
286287

287288
let owner = <Address as TestAddress>::generate(&env);
288289
env.mock_all_auths();
289-
let ip_id = client.commit_ip(&owner, &BytesN::from_array(&env, &[8u8; 32]));
290+
let ip_id = client.commit_ip(&owner, &BytesN::from_array(&env, &[8u8; 32]), &0u32);
290291
client.revoke_ip(&ip_id);
291292
client.revoke_ip(&ip_id); // must panic with IpAlreadyRevoked (code 4)
292293
}
@@ -300,9 +301,9 @@ mod tests {
300301
let client = IpRegistryClient::new(&env, &contract_id);
301302

302303
let owner = <Address as TestAddress>::generate(&env);
303-
let id0 = client.commit_ip(&owner, &BytesN::from_array(&env, &[1u8; 32]));
304-
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[2u8; 32]));
305-
let id2 = client.commit_ip(&owner, &BytesN::from_array(&env, &[3u8; 32]));
304+
let id0 = client.commit_ip(&owner, &BytesN::from_array(&env, &[1u8; 32]), &0u32);
305+
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[2u8; 32]), &0u32);
306+
let id2 = client.commit_ip(&owner, &BytesN::from_array(&env, &[3u8; 32]), &0u32);
306307

307308
assert_eq!(id0, 1);
308309
assert_eq!(id1, 2);
@@ -327,7 +328,7 @@ mod tests {
327328
preimage.append(&soroban_sdk::Bytes::from(blinding.clone()));
328329
let commitment_hash: BytesN<32> = env.crypto().sha256(&preimage).into();
329330

330-
let ip_id = client.commit_ip(&owner, &commitment_hash);
331+
let ip_id = client.commit_ip(&owner, &commitment_hash, &0u32);
331332

332333
// Attempt verification with the wrong secret and assert the check fails.
333334
let wrong_secret = BytesN::from_array(&env, &[99u8; 32]);
@@ -346,9 +347,9 @@ mod tests {
346347
let client = IpRegistryClient::new(&env, &contract_id);
347348

348349
let owner = <Address as TestAddress>::generate(&env);
349-
let id0 = client.commit_ip(&owner, &BytesN::from_array(&env, &[4u8; 32]));
350-
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[5u8; 32]));
351-
let id2 = client.commit_ip(&owner, &BytesN::from_array(&env, &[6u8; 32]));
350+
let id0 = client.commit_ip(&owner, &BytesN::from_array(&env, &[4u8; 32]), &0u32);
351+
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[5u8; 32]), &0u32);
352+
let id2 = client.commit_ip(&owner, &BytesN::from_array(&env, &[6u8; 32]), &0u32);
352353

353354
let ids = client.list_ip_by_owner(&owner);
354355
assert_eq!(ids.len(), 3);
@@ -367,7 +368,7 @@ mod tests {
367368
let owner = <Address as TestAddress>::generate(&env);
368369
let attacker = <Address as TestAddress>::generate(&env);
369370
env.mock_all_auths();
370-
let ip_id = client.commit_ip(&owner, &BytesN::from_array(&env, &[9u8; 32]));
371+
let ip_id = client.commit_ip(&owner, &BytesN::from_array(&env, &[9u8; 32]), &0u32);
371372

372373
// Only mock attacker's auth — owner's auth is absent, must panic
373374
env.mock_auths(&[soroban_sdk::testutils::MockAuth {
@@ -393,7 +394,7 @@ mod tests {
393394
let commitment = BytesN::from_array(&env, &[10u8; 32]);
394395

395396
env.mock_all_auths();
396-
let ip_id = client.commit_ip(&alice, &commitment);
397+
let ip_id = client.commit_ip(&alice, &commitment, &0u32);
397398

398399
// Alice should be the owner
399400
assert!(client.is_ip_owner(&ip_id, &alice));
@@ -426,7 +427,7 @@ mod tests {
426427
let blinding = BytesN::from_array(&env, &[0xcdu8; 32]);
427428
let commitment = make_commitment(&env, &partial_hash, &blinding);
428429

429-
let ip_id = client.commit_ip(&owner, &commitment);
430+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
430431

431432
// Valid proof: returns true
432433
assert!(client.reveal_partial(&ip_id, &partial_hash, &blinding));
@@ -448,7 +449,7 @@ mod tests {
448449
let wrong_blinding = BytesN::from_array(&env, &[0x33u8; 32]);
449450
let commitment = make_commitment(&env, &partial_hash, &blinding);
450451

451-
let ip_id = client.commit_ip(&owner, &commitment);
452+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
452453

453454
// Wrong blinding factor: proof fails
454455
assert!(!client.reveal_partial(&ip_id, &partial_hash, &wrong_blinding));
@@ -470,7 +471,7 @@ mod tests {
470471
let wrong_partial = BytesN::from_array(&env, &[0x66u8; 32]);
471472
let commitment = make_commitment(&env, &partial_hash, &blinding);
472473

473-
let ip_id = client.commit_ip(&owner, &commitment);
474+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
474475

475476
assert!(!client.reveal_partial(&ip_id, &wrong_partial, &blinding));
476477
assert_eq!(client.get_partial_disclosure(&ip_id), None);
@@ -490,7 +491,7 @@ mod tests {
490491
let commitment = make_commitment(&env, &partial_hash, &blinding);
491492

492493
env.mock_all_auths();
493-
let ip_id = client.commit_ip(&owner, &commitment);
494+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
494495

495496
// Only mock attacker's auth — must panic
496497
env.mock_auths(&[soroban_sdk::testutils::MockAuth {
@@ -514,7 +515,7 @@ mod tests {
514515

515516
let owner = <Address as TestAddress>::generate(&env);
516517
let commitment = BytesN::from_array(&env, &[0x99u8; 32]);
517-
let ip_id = client.commit_ip(&owner, &commitment);
518+
let ip_id = client.commit_ip(&owner, &commitment, &0u32);
518519

519520
assert_eq!(client.get_partial_disclosure(&ip_id), None);
520521
}
@@ -587,7 +588,7 @@ mod tests {
587588
let owner = <Address as TestAddress>::generate(&env);
588589

589590
// Single commit
590-
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[10u8; 32]));
591+
let id1 = client.commit_ip(&owner, &BytesN::from_array(&env, &[10u8; 32]), &0u32);
591592
assert_eq!(id1, 1);
592593

593594
// Batch commit 3
@@ -603,7 +604,7 @@ mod tests {
603604
assert_eq!(ids.get(2).unwrap(), 4);
604605

605606
// Another single
606-
let id5 = client.commit_ip(&owner, &BytesN::from_array(&env, &[14u8; 32]));
607+
let id5 = client.commit_ip(&owner, &BytesN::from_array(&env, &[14u8; 32]), &0u32);
607608
assert_eq!(id5, 5);
608609
}
609610

@@ -628,4 +629,92 @@ mod tests {
628629
let zero_hash = BytesN::from_array(&env, &[0u8; 32]);
629630
client.validate_upgrade(&zero_hash);
630631
}
632+
633+
// ── PoW Tests ─────────────────────────────────────────────────────────────
634+
635+
#[test]
636+
fn test_get_pow_difficulty_returns_default_four() {
637+
let env = Env::default();
638+
let contract_id = env.register(crate::IpRegistry, ());
639+
let client = IpRegistryClient::new(&env, &contract_id);
640+
assert_eq!(client.get_pow_difficulty(), 4);
641+
}
642+
643+
#[test]
644+
fn test_commit_ip_pow_difficulty_zero_always_passes() {
645+
let env = Env::default();
646+
env.mock_all_auths();
647+
let contract_id = env.register(crate::IpRegistry, ());
648+
let client = IpRegistryClient::new(&env, &contract_id);
649+
650+
let owner = <Address as TestAddress>::generate(&env);
651+
// Any non-zero hash passes when difficulty is 0
652+
let hash = BytesN::from_array(&env, &[0xffu8; 32]);
653+
let ip_id = client.commit_ip(&owner, &hash, &0u32);
654+
assert_eq!(ip_id, 1);
655+
}
656+
657+
#[test]
658+
fn test_commit_ip_pow_difficulty_eight_accepts_leading_zero_byte() {
659+
let env = Env::default();
660+
env.mock_all_auths();
661+
let contract_id = env.register(crate::IpRegistry, ());
662+
let client = IpRegistryClient::new(&env, &contract_id);
663+
664+
let owner = <Address as TestAddress>::generate(&env);
665+
// First byte 0x00 = 8 leading zero bits — satisfies difficulty 8
666+
let mut hash_bytes = [0x01u8; 32];
667+
hash_bytes[0] = 0x00;
668+
let hash = BytesN::from_array(&env, &hash_bytes);
669+
let ip_id = client.commit_ip(&owner, &hash, &8u32);
670+
assert_eq!(ip_id, 1);
671+
}
672+
673+
#[test]
674+
fn test_commit_ip_pow_difficulty_four_accepts_half_zero_nibble() {
675+
let env = Env::default();
676+
env.mock_all_auths();
677+
let contract_id = env.register(crate::IpRegistry, ());
678+
let client = IpRegistryClient::new(&env, &contract_id);
679+
680+
let owner = <Address as TestAddress>::generate(&env);
681+
// 0x0f = 0000_1111 — 4 leading zero bits, satisfies difficulty 4
682+
let mut hash_bytes = [0x01u8; 32];
683+
hash_bytes[0] = 0x0f;
684+
let hash = BytesN::from_array(&env, &hash_bytes);
685+
let ip_id = client.commit_ip(&owner, &hash, &4u32);
686+
assert_eq!(ip_id, 1);
687+
}
688+
689+
#[test]
690+
#[should_panic]
691+
fn test_commit_ip_pow_difficulty_four_rejects_insufficient_leading_zeros() {
692+
let env = Env::default();
693+
env.mock_all_auths();
694+
let contract_id = env.register(crate::IpRegistry, ());
695+
let client = IpRegistryClient::new(&env, &contract_id);
696+
697+
let owner = <Address as TestAddress>::generate(&env);
698+
// 0x1f = 0001_1111 — only 3 leading zero bits, fails difficulty 4
699+
let mut hash_bytes = [0x01u8; 32];
700+
hash_bytes[0] = 0x1f;
701+
let hash = BytesN::from_array(&env, &hash_bytes);
702+
client.commit_ip(&owner, &hash, &4u32);
703+
}
704+
705+
#[test]
706+
#[should_panic]
707+
fn test_commit_ip_pow_difficulty_one_rejects_high_bit_set() {
708+
let env = Env::default();
709+
env.mock_all_auths();
710+
let contract_id = env.register(crate::IpRegistry, ());
711+
let client = IpRegistryClient::new(&env, &contract_id);
712+
713+
let owner = <Address as TestAddress>::generate(&env);
714+
// 0x80 = 1000_0000 — high bit set, fails difficulty 1
715+
let mut hash_bytes = [0x01u8; 32];
716+
hash_bytes[0] = 0x80;
717+
let hash = BytesN::from_array(&env, &hash_bytes);
718+
client.commit_ip(&owner, &hash, &1u32);
719+
}
631720
}

0 commit comments

Comments
 (0)