diff --git a/TODO.md b/TODO.md index 8f3022a..ac9d2ff 100644 --- a/TODO.md +++ b/TODO.md @@ -1,44 +1,14 @@ -# TrustLink Performance Benchmarking TODO (Issue 88) +# TODO: Implement Attestation Search by Claim Type (#24) -**Progress: 7/7 complete** ✅ +## Steps: +- [ ] 1. Add StorageKey::ClaimTypeAttestations(String) in src/storage.rs +- [ ] 2. Add get_claim_type_attestations, add_claim_type_attestation in src/storage.rs +- [ ] 3. Update store_attestation in src/lib.rs to index claim_type +- [ ] 4. Add get_subjects_by_claim_type in src/lib.rs impl +- [ ] 5. Add unit tests in src/test.rs +- [ ] 6. Update README.md +- [ ] 7. cargo test +- [ ] 8. git commit, gh pr create -## 1. Create benchmark harness [IN PROGRESS] -- [ ] Create `benches/performance.rs` with CU measurements for all functions - - create_attestation (baseline) - - revoke_attestation (baseline) - - has_valid_claim (1, 10, 100 attestations) - - get_subject_attestations (page sizes: 10, 50, 100, full) +Current: Step 1. -## 2. Setup test environment -- [ ] Deploy TrustLink contract in test Env -- [ ] Register issuer/admin -- [ ] Generate noise attestations for realistic scenarios - -## 3. Run create_attestation benchmark -- [ ] Measure baseline CU -- [ ] Test with metadata/tags for variance - -## 4. Run revoke_attestation benchmark -- [ ] Measure baseline CU on fresh attestation - -## 5. Run has_valid_claim benchmarks -- [ ] 1 attestation (trivial case) -- [ ] 10 attestations (1 valid + 9 noise) -- [ ] 100 attestations (1 valid + 99 noise) -- [ ] Invalid claim case - -## 6. Run get_subject_attestations benchmarks -- [ ] Various page sizes (10/50/full) -- [ ] Large lists (500 attestations) - -## 7. Document results + optimizations -- [ ] Create `docs/performance.md` -- [ ] Storage byte estimates -- [ ] Optimization recommendations -- [ ] attempt_completion - -**Next Step:** Create benchmark file and implement first measurements** - - - -/home/emjay/Desktop/TrustLink/TrustLink-Issue88/benches/performance.rs diff --git a/src/lib.rs b/src/lib.rs index d2bbe28..b52cad4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1596,218 +1596,64 @@ impl TrustLinkContract { } } - // ------------------------------------------------------------------------- - // Pull-based attestation requests - // ------------------------------------------------------------------------- - - /// Submit an attestation request from `subject` to a registered `issuer`. + /// Transfer ownership of an attestation to a new issuer (admin only). /// - /// The request is stored on-chain and expires after - /// [`ATTESTATION_REQUEST_TTL_SECS`] seconds if not acted on. + /// Used when an issuer is removed/deactivated, allowing admin to re-assign orphaned + /// attestations to a new issuer. Updates issuer field, indexes, stats, emits event. /// /// # Errors - /// - [`Error::NotInitialized`] — contract not initialized. - /// - [`Error::ContractPaused`] — contract is paused. - /// - [`Error::Unauthorized`] — `issuer` is not a registered issuer. - /// - [`Error::DuplicateRequest`] — an identical pending request already exists. - pub fn request_attestation( + /// [`Error::Unauthorized`] if caller is not admin or new_issuer not registered. + /// [`Error::NotFound`] if attestation_id does not exist. + pub fn transfer_attestation( env: Env, - subject: Address, - issuer: Address, - claim_type: String, - ) -> Result { - subject.require_auth(); - Validation::require_not_paused(&env)?; - Validation::require_issuer(&env, &issuer)?; - validate_claim_type(&claim_type)?; - - let timestamp = env.ledger().timestamp(); - let id = AttestationRequest::generate_id(&env, &subject, &issuer, &claim_type, timestamp); - - // Reject if a pending request with the same ID already exists. - if let Ok(existing) = Storage::get_attestation_request(&env, &id) { - if existing.status == RequestStatus::Pending { - return Err(Error::DuplicateRequest); - } - } - - let expires_at = timestamp + ATTESTATION_REQUEST_TTL_SECS; - let request = AttestationRequest { - id: id.clone(), - subject, - issuer: issuer.clone(), - claim_type, - timestamp, - expires_at, - status: RequestStatus::Pending, - rejection_reason: None, - }; - - Storage::set_attestation_request(&env, &request); - Storage::add_issuer_request(&env, &issuer, &id); + admin: Address, + attestation_id: String, + new_issuer: Address, + ) -> Result<(), Error> { + admin.require_auth(); + Validation::require_admin(&env, &admin)?; - Events::attestation_requested( - &env, - &request.id, - &request.subject, - &request.issuer, - &request.claim_type, - expires_at, - ); + let mut attestation = Storage::get_attestation(&env, &attestation_id)?; + let old_issuer = attestation.issuer.clone(); - Ok(id) - } + Validation::require_issuer(&env, &new_issuer)?; - /// Fulfill a pending attestation request, creating a standard attestation. - /// - /// # Errors - /// - [`Error::NotFound`] — request does not exist. - /// - [`Error::Unauthorized`] — caller is not the request's target issuer. - /// - [`Error::RequestAlreadyProcessed`] — request was already fulfilled or rejected. - /// - [`Error::RequestExpired`] — request expired before being acted on. - pub fn fulfill_request( - env: Env, - issuer: Address, - request_id: String, - expiration: Option, - ) -> Result { - issuer.require_auth(); - Validation::require_not_paused(&env)?; + if old_issuer == new_issuer { + return Ok(()); + } - let mut request = Storage::get_attestation_request(&env, &request_id)?; + // Update indexes + Storage::remove_issuer_attestation(&env, &old_issuer, &attestation_id); + let mut old_stats = Storage::get_issuer_stats(&env, &old_issuer); + old_stats.total_issued = old_stats.total_issued.saturating_sub(1); + Storage::set_issuer_stats(&env, &old_issuer, &old_stats); - if request.issuer != issuer { - return Err(Error::Unauthorized); - } - if request.status != RequestStatus::Pending { - return Err(Error::RequestAlreadyProcessed); - } - if env.ledger().timestamp() >= request.expires_at { - return Err(Error::RequestExpired); - } + attestation.issuer = new_issuer.clone(); + Storage::set_attestation(&env, &attestation); - validate_native_expiration(&env, expiration)?; - charge_attestation_fee(&env, &issuer)?; + Storage::add_issuer_attestation(&env, &new_issuer, &attestation_id); + let mut new_stats = Storage::get_issuer_stats(&env, &new_issuer); + new_stats.total_issued += 1; + Storage::set_issuer_stats(&env, &new_issuer, &new_stats); + // Event and audit + Events::attestation_transferred(&env, &attestation_id, &old_issuer, &new_issuer); let timestamp = env.ledger().timestamp(); - let attestation_id = Attestation::generate_id( + Storage::append_audit_entry( &env, - &issuer, - &request.subject, - &request.claim_type, - timestamp, + &attestation_id, + &AuditEntry { + action: AuditAction::Transferred, + actor: admin.clone(), + timestamp, + details: Some(format!( + "{}", + new_issuer.to_string() + )), + }, ); - let attestation = Attestation { - id: attestation_id.clone(), - issuer: issuer.clone(), - subject: request.subject.clone(), - claim_type: request.claim_type.clone(), - timestamp, - expiration, - revoked: false, - metadata: None, - valid_from: None, - imported: false, - bridged: false, - source_chain: None, - source_tx: None, - tags: None, - revocation_reason: None, - deleted: false, - }; - - store_attestation(&env, &attestation); - Storage::increment_total_attestations(&env, 1); - - let audit_entry = AuditEntry { - action: AuditAction::Created, - actor: issuer.clone(), - timestamp, - details: None, - }; - Storage::append_audit_entry(&env, &attestation_id, &audit_entry); - - Events::attestation_created(&env, &attestation); - - // Mark request fulfilled. - request.status = RequestStatus::Fulfilled; - Storage::set_attestation_request(&env, &request); - - Events::request_fulfilled(&env, &request_id, &issuer, &attestation_id); - - Ok(attestation_id) - } - - /// Reject a pending attestation request. - /// - /// # Errors - /// - [`Error::NotFound`] — request does not exist. - /// - [`Error::Unauthorized`] — caller is not the request's target issuer. - /// - [`Error::RequestAlreadyProcessed`] — request was already fulfilled or rejected. - /// - [`Error::ReasonTooLong`] — rejection reason exceeds 128 characters. - pub fn reject_request( - env: Env, - issuer: Address, - request_id: String, - reason: Option, - ) -> Result<(), Error> { - issuer.require_auth(); - - let mut request = Storage::get_attestation_request(&env, &request_id)?; - - if request.issuer != issuer { - return Err(Error::Unauthorized); - } - if request.status != RequestStatus::Pending { - return Err(Error::RequestAlreadyProcessed); - } - - validate_reason(&reason)?; - - request.status = RequestStatus::Rejected; - request.rejection_reason = reason.clone(); - Storage::set_attestation_request(&env, &request); - - Events::request_rejected(&env, &request_id, &issuer, &reason); - Ok(()) } - - /// Return a paginated list of pending request IDs for `issuer`. - /// - /// Expired or processed requests are filtered out of the results. - pub fn get_pending_requests( - env: Env, - issuer: Address, - start: u32, - limit: u32, - ) -> Vec { - let now = env.ledger().timestamp(); - let all_ids = Storage::get_issuer_requests(&env, &issuer); - - // Collect only IDs whose request is still pending and not expired. - let mut pending = Vec::new(&env); - for id in all_ids.iter() { - if let Ok(req) = Storage::get_attestation_request(&env, &id) { - if req.status == RequestStatus::Pending && now < req.expires_at { - pending.push_back(id); - } - } - } - - crate::storage::paginate(&env, pending, start, limit) - } - - /// Retrieve a single attestation request by ID. - /// - /// # Errors - /// - [`Error::NotFound`] — no request with that ID exists. - pub fn get_attestation_request( - env: Env, - request_id: String, - ) -> Result { - Storage::get_attestation_request(&env, &request_id) - } }