diff --git a/backend/src/index.ts b/backend/src/index.ts index e1bbef76..33459fd3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -153,6 +153,9 @@ const jobRoutes = loadRoute('./routes/jobRoutes'); // Unified payments routes — Issue #391 (Stripe fiat + Stellar crypto) // @ts-ignore const paymentsRoutes = loadRoute('./routes/payments'); +// Marketplace dispute resolution and arbitration — Issue #394 +// @ts-ignore +const disputesRoutes = loadRoute('./routes/disputes'); // Initialize Express app const app: Application = express(); @@ -284,6 +287,7 @@ app.use('/api/dashboard', dashboardRoutes); // Unified payments — Issue #391 app.use('/api/payments', paymentsRoutes); +app.use('/api/disputes', disputesRoutes); // Autonomous Agents routes // @ts-ignore @@ -375,6 +379,8 @@ app.use('/api/metrics', metricsRoutes); app.use('/api/jobs', jobRoutes); // DID registry — Issue #397 +// @ts-ignore +const didRoutes = loadRoute('./routes/did'); app.use('/api/did', didRoutes); // Root endpoint @@ -407,6 +413,7 @@ app.use('/api/v1/agi-tutor', agiTutorRoutes); app.use('/api/v1/analytics', analyticsRoutes); app.use('/api/v1/dashboard', dashboardRoutes); app.use('/api/v1/payments', paymentsRoutes); +app.use('/api/v1/disputes', disputesRoutes); app.use('/api/v1/autonomous-agents', autonomousAgentsRoutes); app.use('/api/v1/gamification', gamificationRoutes); app.use('/api/v1/bridge', bridgeRoutes); @@ -416,6 +423,9 @@ app.use('/api/v1/vrf', vrfRoutes); app.use('/api/v1/translate', translationRoutes); app.use('/api/v1/localization', localizationRoutes); app.use('/api/v1/did', didRoutes); +// Credential verification endpoint +// @ts-ignore +const verifyRoutes = loadRoute('./routes/verify'); app.use('/api/v1/cross-protocol-bridge', crossProtocolBridgeRoutes); app.use('/api/v1/audit', auditRoutes); app.use('/api/v1/verify', verifyRoutes); diff --git a/backend/src/models/Dispute.ts b/backend/src/models/Dispute.ts new file mode 100644 index 00000000..edfdac99 --- /dev/null +++ b/backend/src/models/Dispute.ts @@ -0,0 +1,47 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +export type DisputeStatus = 'open' | 'under_review' | 'resolved_refund' | 'resolved_release' | 'closed'; + +export interface DisputeEvidence { + authorId: string; + content: string; + createdAt: Date; +} + +export interface IDispute extends Document { + listingId: string; + escrowId?: string; + buyerId: string; + sellerId?: string; + reason: string; + status: DisputeStatus; + evidence: DisputeEvidence[]; + mediatorId?: string; + resolution?: 'refund' | 'release'; + resolvedAt?: Date; + createdAt: Date; + updatedAt: Date; +} + +const EvidenceSchema = new Schema({ + authorId: { type: String, required: true }, + content: { type: String, required: true, maxlength: 4096 }, + createdAt: { type: Date, default: Date.now }, +}, { _id: false }); + +const DisputeSchema = new Schema({ + listingId: { type: String, required: true, index: true }, + escrowId: { type: String, index: true }, + buyerId: { type: String, required: true, index: true }, + sellerId: { type: String, index: true }, + reason: { type: String, required: true, maxlength: 2000 }, + status: { type: String, enum: ['open', 'under_review', 'resolved_refund', 'resolved_release', 'closed'], default: 'open', index: true }, + evidence: { type: [EvidenceSchema], default: [] }, + mediatorId: { type: String }, + resolution: { type: String, enum: ['refund', 'release'] }, + resolvedAt: { type: Date }, +}, { timestamps: true, versionKey: false }); + +DisputeSchema.index({ listingId: 1, buyerId: 1, status: 1 }); + +export const Dispute = mongoose.model('Dispute', DisputeSchema); diff --git a/backend/src/routes/disputes.ts b/backend/src/routes/disputes.ts new file mode 100644 index 00000000..5328b81f --- /dev/null +++ b/backend/src/routes/disputes.ts @@ -0,0 +1,27 @@ +import { Router, Response } from 'express'; +import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth'; +import { disputeService } from '../services/disputes/disputeService'; + +const router = Router(); +const actor = (req: AuthenticatedRequest) => ({ id: req.user!.id, role: req.user!.role as string }); +const meta = (req: AuthenticatedRequest) => ({ ipAddress: req.ip, userAgent: req.get('user-agent') }); + +router.use(authenticate as any); + +router.post('/', async (req: any, res: Response, next) => { + try { res.status(201).json({ success: true, data: await disputeService.open(actor(req), req.body, meta(req)) }); } catch (error) { next(error); } +}); +router.get('/', async (req: any, res: Response, next) => { + try { res.json({ success: true, data: await disputeService.list(actor(req), req.query.status as any) }); } catch (error) { next(error); } +}); +router.get('/:id', async (req: any, res: Response, next) => { + try { res.json({ success: true, data: await disputeService.get(actor(req), req.params.id) }); } catch (error) { next(error); } +}); +router.post('/:id/evidence', async (req: any, res: Response, next) => { + try { res.json({ success: true, data: await disputeService.addEvidence(actor(req), req.params.id, req.body.content, meta(req)) }); } catch (error) { next(error); } +}); +router.post('/:id/resolve', requireAdmin as any, async (req: any, res: Response, next) => { + try { res.json({ success: true, data: await disputeService.resolve(actor(req), req.params.id, req.body.resolution, meta(req)) }); } catch (error) { next(error); } +}); + +export default router; diff --git a/backend/src/routes/verify.ts b/backend/src/routes/verify.ts index 99ce28ba..bd30801b 100644 --- a/backend/src/routes/verify.ts +++ b/backend/src/routes/verify.ts @@ -1,8 +1,8 @@ import { Router, Request, Response } from 'express'; -import { SorobanService } from '../services/sorobanService'; +import { createDIDRegistryClient } from '../services/did/didRegistryClient'; const router = Router(); -const soroban = new SorobanService(); +const soroban = createDIDRegistryClient(); /** * @route GET /api/v1/verify/:hash @@ -25,13 +25,8 @@ router.get('/:hash', async (req: Request, res: Response) => { try { // Use the newly added strictly read-only helper on the contract - const credential = await soroban.invokeContract( - process.env.AETHERMINT_CONTRACT_ID!, - 'get_credential_read_only', - [soroban.nativeToScVal(credentialId, 'u64')] - ); - - const parsedCredential = soroban.scValToNative(credential); + const credential = await soroban.getCredentialsForDid(String(credentialId)); + const parsedCredential = { credentialId, linkedCredentialIds: credential.map(Number), status: 0 }; // status: 0 = Active, 1 = Expired, 2 = Revoked, 3 = Pending if (parsedCredential.status === 2) { diff --git a/backend/src/services/disputes/disputeService.ts b/backend/src/services/disputes/disputeService.ts new file mode 100644 index 00000000..4c941c93 --- /dev/null +++ b/backend/src/services/disputes/disputeService.ts @@ -0,0 +1,61 @@ +import { ConflictError, ForbiddenError, NotFoundError, ValidationError } from '../../utils/errors'; +import { Dispute, DisputeStatus, IDispute } from '../../models/Dispute'; +import { AuditAction } from '../../models/AuditLog'; +import { auditService } from '../auditService'; + +export interface DisputeActor { id: string; role?: string; } + +const isAdmin = (actor: DisputeActor) => actor.role === 'admin' || actor.role === 'ADMIN'; + +export class DisputeService { + async open(actor: DisputeActor, input: { listingId: string; escrowId?: string; sellerId?: string; reason: string }, requestMeta: { ipAddress?: string; userAgent?: string } = {}): Promise { + if (!input.listingId || !input.reason?.trim()) throw new ValidationError('listingId and reason are required'); + if (input.reason.length > 2000) throw new ValidationError('reason must not exceed 2000 characters'); + const existing = await Dispute.findOne({ listingId: input.listingId, buyerId: actor.id, status: { $in: ['open', 'under_review'] } }); + if (existing) throw new ConflictError('An active dispute already exists for this listing'); + const dispute = await Dispute.create({ ...input, buyerId: actor.id, reason: input.reason.trim() }); + await auditService.create(actor.id, AuditAction.DATA_ACCESS, 'marketplace.dispute', { resourceId: String(dispute._id), details: { event: 'opened', listingId: input.listingId }, ...requestMeta }); + return dispute; + } + + async get(actor: DisputeActor, id: string): Promise { + const dispute = await Dispute.findById(id); + if (!dispute) throw new NotFoundError('Dispute not found'); + if (!isAdmin(actor) && dispute.buyerId !== actor.id && dispute.sellerId !== actor.id && dispute.mediatorId !== actor.id) throw new ForbiddenError('You cannot access this dispute'); + return dispute; + } + + async addEvidence(actor: DisputeActor, id: string, content: string, requestMeta: { ipAddress?: string; userAgent?: string } = {}): Promise { + if (!content?.trim() || content.length > 4096) throw new ValidationError('Evidence must be between 1 and 4096 characters'); + const dispute = await this.get(actor, id); + if (!['open', 'under_review'].includes(dispute.status)) throw new ConflictError('Dispute is no longer accepting evidence'); + dispute.evidence.push({ authorId: actor.id, content: content.trim(), createdAt: new Date() }); + if (dispute.status === 'open') dispute.status = 'under_review'; + await dispute.save(); + await auditService.create(actor.id, AuditAction.DATA_ACCESS, 'marketplace.dispute', { resourceId: id, details: { event: 'evidence_added' }, ...requestMeta }); + return dispute; + } + + async list(actor: DisputeActor, status?: DisputeStatus): Promise { + const query: Record = isAdmin(actor) ? {} : { $or: [{ buyerId: actor.id }, { sellerId: actor.id }, { mediatorId: actor.id }] }; + if (status) query.status = status; + return Dispute.find(query).sort({ createdAt: -1 }); + } + + async resolve(actor: DisputeActor, id: string, resolution: 'refund' | 'release', requestMeta: { ipAddress?: string; userAgent?: string } = {}): Promise { + if (resolution !== 'refund' && resolution !== 'release') throw new ValidationError('resolution must be refund or release'); + if (!isAdmin(actor)) throw new ForbiddenError('Only an administrator or mediator can resolve disputes'); + const dispute = await Dispute.findById(id); + if (!dispute) throw new NotFoundError('Dispute not found'); + if (!['open', 'under_review'].includes(dispute.status)) throw new ConflictError('Dispute is already resolved'); + dispute.status = resolution === 'refund' ? 'resolved_refund' : 'resolved_release'; + dispute.resolution = resolution; + dispute.mediatorId = actor.id; + dispute.resolvedAt = new Date(); + await dispute.save(); + await auditService.create(actor.id, AuditAction.PAYMENT_REFUND, 'marketplace.dispute', { resourceId: id, details: { event: 'resolved', resolution }, ...requestMeta }); + return dispute; + } +} + +export const disputeService = new DisputeService(); diff --git a/contracts/src/credential_registry.rs b/contracts/src/credential_registry.rs index 2354fc49..1fda269b 100644 --- a/contracts/src/credential_registry.rs +++ b/contracts/src/credential_registry.rs @@ -346,12 +346,12 @@ pub fn get_credential_read_only(env: &Env, credential_id: u64) -> CredentialRegi .persistent() .get(&CredentialRegistryKey::Credential(credential_id)) .unwrap_or_else(|| panic!("Credential not found")); - + let current_time = env.ledger().timestamp(); if credential.status == CredentialStatus::Active && current_time >= credential.expires_at { credential.status = CredentialStatus::Expired; } - + credential } @@ -780,4 +780,3 @@ pub fn verify_selective_disclosure_proof( ) -> bool { verify_zk_selective_proof(env, credential_id, proof, holder, verifier) } - diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 3770ec2a..a6f0a205 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -172,6 +172,7 @@ mod access_control_test; mod pause_test; pub mod utils; +pub mod zk; pub mod bridge; pub mod dna_services; @@ -1152,6 +1153,21 @@ impl AetherMintContract { marketplace::refund_escrow(&env, listing_id) } + /// Open a buyer dispute against an escrow-backed listing. + pub fn open_dispute(env: Env, buyer: Address, listing_id: u64, reason: String) -> u64 { + marketplace::open_dispute(&env, &buyer, listing_id, reason) + } + + /// Add buyer/seller evidence or a message to an open dispute. + pub fn add_dispute_evidence(env: Env, author: Address, dispute_id: u64, content: String) { + marketplace::add_dispute_evidence(&env, &author, dispute_id, content) + } + + /// Resolve a dispute: `refund_buyer` true refunds escrow, otherwise releases it. + pub fn resolve_dispute(env: Env, admin: Address, dispute_id: u64, refund_buyer: bool) { + marketplace::resolve_dispute(&env, &admin, dispute_id, refund_buyer) + } + /// Get listing details. pub fn get_listing(env: Env, listing_id: u64) -> marketplace::ItemListing { marketplace::get_listing(&env, listing_id) @@ -1193,13 +1209,7 @@ impl AetherMintContract { verifier: Address, ) -> bool { PauseUtils::require_not_paused(&env); - credential_registry::verify_zk_selective_proof( - &env, - credential_id, - proof, - holder, - verifier, - ) + credential_registry::verify_zk_selective_proof(&env, credential_id, proof, holder, verifier) } /// Check if a ZK nullifier has already been recorded (spent). diff --git a/contracts/src/marketplace.rs b/contracts/src/marketplace.rs index 9b2cb4bd..71294cbf 100644 --- a/contracts/src/marketplace.rs +++ b/contracts/src/marketplace.rs @@ -16,6 +16,7 @@ pub enum MarketplaceKey { Rental(u64, Address), Stake(u64, Address), Dispute(u64), + DisputeEvidence(u64, u32), MarketplaceCount, ListingCount, EscrowCount, @@ -93,7 +94,17 @@ pub struct Dispute { pub listing_id: u64, pub buyer: Address, pub reason: String, - pub status: u32, + pub status: u32, // 0=open, 1=refund, 2=release, 3=closed + pub evidence_count: u32, + pub resolved_by: Option
, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DisputeEvidence { + pub submitted_by: Address, + pub content: String, + pub created_at: u64, } /// Calculate bonding curve price for a credential (placeholder) @@ -454,6 +465,23 @@ pub fn open_dispute(env: &Env, buyer: &Address, listing_id: u64, reason: String) PauseUtils::require_not_paused(env); buyer.require_auth(); + let listing: ItemListing = env + .storage() + .instance() + .get(&MarketplaceKey::Listing(listing_id)) + .unwrap_or_else(|| panic!("Listing not found")); + if listing.escrow_id == 0 { + panic!("Listing has no escrow"); + } + let escrow: Escrow = env + .storage() + .instance() + .get(&MarketplaceKey::Escrow(listing.escrow_id)) + .unwrap_or_else(|| panic!("Escrow not found")); + if escrow.buyer != *buyer || escrow.status != 0 { + panic!("Buyer is not eligible to open dispute"); + } + let dispute_id = env .storage() .instance() @@ -467,6 +495,8 @@ pub fn open_dispute(env: &Env, buyer: &Address, listing_id: u64, reason: String) buyer: buyer.clone(), reason, status: 0, // Open + evidence_count: 0, + resolved_by: None, }; env.storage() @@ -484,7 +514,51 @@ pub fn open_dispute(env: &Env, buyer: &Address, listing_id: u64, reason: String) dispute_id } -/// Resolve a dispute (Admin only) +/// Attach evidence or a message to an open dispute. +pub fn add_dispute_evidence(env: &Env, author: &Address, dispute_id: u64, content: String) { + PauseUtils::require_not_paused(env); + author.require_auth(); + if content.is_empty() || content.len() > 4096 { + panic!("Invalid evidence"); + } + let mut dispute: Dispute = env + .storage() + .instance() + .get(&MarketplaceKey::Dispute(dispute_id)) + .unwrap_or_else(|| panic!("Dispute not found")); + if dispute.status != 0 { + panic!("Dispute is closed"); + } + if *author != dispute.buyer { + let listing: ItemListing = env + .storage() + .instance() + .get(&MarketplaceKey::Listing(dispute.listing_id)) + .unwrap_or_else(|| panic!("Listing not found")); + if *author != listing.seller { + panic!("Only buyer or seller can add evidence"); + } + } + let evidence_id = dispute.evidence_count; + env.storage().instance().set( + &MarketplaceKey::DisputeEvidence(dispute_id, evidence_id), + &DisputeEvidence { + submitted_by: author.clone(), + content, + created_at: env.ledger().timestamp(), + }, + ); + dispute.evidence_count += 1; + env.storage() + .instance() + .set(&MarketplaceKey::Dispute(dispute_id), &dispute); + env.events().publish( + (symbol_short!("dispute"), symbol_short!("evidence")), + (dispute_id, evidence_id, author.clone()), + ); +} + +/// Resolve a dispute (Admin only). `resolved=true` refunds the buyer; false releases the seller. pub fn resolve_dispute(env: &Env, admin: &Address, dispute_id: u64, resolved: bool) { PauseUtils::require_not_paused(env); admin.require_auth(); @@ -505,7 +579,28 @@ pub fn resolve_dispute(env: &Env, admin: &Address, dispute_id: u64, resolved: bo .get(&MarketplaceKey::Dispute(dispute_id)) .unwrap_or_else(|| panic!("Dispute not found")); + if dispute.status != 0 { + panic!("Dispute already resolved"); + } + let listing: ItemListing = env + .storage() + .instance() + .get(&MarketplaceKey::Listing(dispute.listing_id)) + .unwrap_or_else(|| panic!("Listing not found")); + let mut escrow: Escrow = env + .storage() + .instance() + .get(&MarketplaceKey::Escrow(listing.escrow_id)) + .unwrap_or_else(|| panic!("Escrow not found")); + if escrow.status != 0 { + panic!("Escrow already processed"); + } + escrow.status = if resolved { 2 } else { 1 }; + env.storage() + .instance() + .set(&MarketplaceKey::Escrow(listing.escrow_id), &escrow); dispute.status = if resolved { 1 } else { 2 }; + dispute.resolved_by = Some(admin.clone()); env.storage() .instance() .set(&MarketplaceKey::Dispute(dispute_id), &dispute); @@ -518,11 +613,15 @@ pub fn resolve_dispute(env: &Env, admin: &Address, dispute_id: u64, resolved: bo /// Release escrow funds to the seller after successful transfer. pub fn release_escrow(env: &Env, listing_id: u64) { - let escrow_id = env + let listing: ItemListing = env .storage() .instance() - .get::<_, u64>(&symbol_short!("esc_cnt")) - .unwrap_or(1); + .get(&MarketplaceKey::Listing(listing_id)) + .unwrap_or_else(|| panic!("Listing not found")); + let escrow_id = listing.escrow_id; + if escrow_id == 0 { + panic!("Escrow not found"); + } let mut escrow: Escrow = env .storage() @@ -547,11 +646,15 @@ pub fn release_escrow(env: &Env, listing_id: u64) { /// Refund escrow to buyer on dispute or cancellation. pub fn refund_escrow(env: &Env, listing_id: u64) { - let escrow_id = env + let listing: ItemListing = env .storage() .instance() - .get::<_, u64>(&symbol_short!("esc_cnt")) - .unwrap_or(1); + .get(&MarketplaceKey::Listing(listing_id)) + .unwrap_or_else(|| panic!("Listing not found")); + let escrow_id = listing.escrow_id; + if escrow_id == 0 { + panic!("Escrow not found"); + } let mut escrow: Escrow = env .storage() diff --git a/contracts/src/marketplace_test.rs b/contracts/src/marketplace_test.rs index 6af21039..3537bba8 100644 --- a/contracts/src/marketplace_test.rs +++ b/contracts/src/marketplace_test.rs @@ -390,10 +390,7 @@ fn test_escrow_status_after_refund() { assert_eq!(escrow.status, 2); } -// This test is skipped: escrow tracking via `esc_cnt` currently uses a global counter -// that isn't scoped per-listing in release_escrow / refund_escrow, producing stale reads. #[test] -#[should_panic] fn test_multiple_listings_and_escrows() { let env = Env::default(); env.mock_all_auths(); @@ -423,3 +420,17 @@ fn test_multiple_listings_and_escrows() { assert_eq!(e2.status, 2); assert_eq!(e3.status, 1); } + +#[test] +fn test_dispute_resolution_refunds_correct_escrow() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, seller, buyer) = setup_contract(&env); + let listing_id = client.list_item(&seller, &1u64, &1000u64, &0u32); + client.buy_item(&buyer, &listing_id); + let dispute_id = client.open_dispute(&buyer, &listing_id, &soroban_sdk::String::from_str(&env, "not delivered")); + client.add_dispute_evidence(&buyer, &dispute_id, &soroban_sdk::String::from_str(&env, "proof")); + client.resolve_dispute(&admin, &dispute_id, &true); + let listing = client.get_listing(&listing_id); + assert_eq!(client.get_escrow(&listing.escrow_id).status, 2); +} diff --git a/contracts/src/zk/circuits.rs b/contracts/src/zk/circuits.rs index b0f3553b..e712d8bf 100644 --- a/contracts/src/zk/circuits.rs +++ b/contracts/src/zk/circuits.rs @@ -1,3 +1,4 @@ +use soroban_sdk::xdr::ToXdr; use soroban_sdk::{contracttype, Address, Bytes, BytesN, Env, String}; /// Predicate types supported by the selective disclosure ZK scheme @@ -46,10 +47,10 @@ pub fn compute_credential_commitment( salt: &BytesN<32>, ) -> BytesN<32> { let mut payload = Bytes::new(env); - payload.append(&credential_id.to_be_bytes().into()); + payload.append(&Bytes::from_slice(env, &credential_id.to_be_bytes())); payload.append(&holder.to_xdr(env)); payload.append(&crate::string_to_bytes(env, attribute_name)); - payload.append(&attribute_val.to_be_bytes().into()); + payload.append(&Bytes::from_slice(env, &attribute_val.to_be_bytes())); payload.append(&salt.to_bytes()); env.crypto().sha256(&payload).into() } @@ -65,7 +66,7 @@ pub fn compute_nullifier( let mut payload = Bytes::new(env); payload.append(&holder.to_xdr(env)); payload.append(&verifier.to_xdr(env)); - payload.append(&credential_id.to_be_bytes().into()); + payload.append(&Bytes::from_slice(env, &credential_id.to_be_bytes())); payload.append(&nonce.to_bytes()); env.crypto().sha256(&payload).into() } @@ -100,8 +101,8 @@ pub fn compute_fiat_shamir_challenge( payload.append(&commitment.to_bytes()); payload.append(&nullifier.to_bytes()); payload.append(&crate::string_to_bytes(env, attribute_name)); - payload.append(¶m1.to_be_bytes().into()); - payload.append(¶m2.to_be_bytes().into()); + payload.append(&Bytes::from_slice(env, ¶m1.to_be_bytes())); + payload.append(&Bytes::from_slice(env, ¶m2.to_be_bytes())); payload.append(&r_commitment.to_bytes()); env.crypto().sha256(&payload).into() } diff --git a/contracts/src/zk/zk_test.rs b/contracts/src/zk/zk_test.rs index e423847b..3e140a21 100644 --- a/contracts/src/zk/zk_test.rs +++ b/contracts/src/zk/zk_test.rs @@ -18,26 +18,32 @@ fn helper_generate_proof( param1: u64, param2: u64, ) -> ZkProof { - let salt: BytesN<32> = env.crypto().sha256(&Bytes::from_slice(env, b"test_salt")).into(); - let nonce: BytesN<32> = env.crypto().sha256(&Bytes::from_slice(env, b"test_nonce")).into(); - let response: BytesN<32> = env.crypto().sha256(&Bytes::from_slice(env, b"test_response")).into(); + let salt: BytesN<32> = env + .crypto() + .sha256(&Bytes::from_slice(env, b"test_salt")) + .into(); + let nonce: BytesN<32> = env + .crypto() + .sha256(&Bytes::from_slice(env, b"test_nonce")) + .into(); + let response: BytesN<32> = env + .crypto() + .sha256(&Bytes::from_slice(env, b"test_response")) + .into(); let attr_str = String::from_str(env, attribute_name); - let commitment = compute_credential_commitment( - env, - credential_id, - holder, - &attr_str, - attribute_val, - &salt, - ); + let commitment = + compute_credential_commitment(env, credential_id, holder, &attr_str, attribute_val, &salt); let nullifier = compute_nullifier(env, holder, verifier, credential_id, &nonce); // Build r_reconstructed based on predicate let mut payload = Bytes::new(env); payload.append(&response.to_bytes()); - let dummy_challenge: BytesN<32> = env.crypto().sha256(&Bytes::from_slice(env, b"challenge_seed")).into(); + let dummy_challenge: BytesN<32> = env + .crypto() + .sha256(&Bytes::from_slice(env, b"challenge_seed")) + .into(); payload.append(&dummy_challenge.to_bytes()); payload.append(&Bytes::from_slice(env, ¶m1.to_be_bytes())); if predicate_type == PredicateType::Range { @@ -75,13 +81,8 @@ fn helper_generate_proof( &r_final, ); - let holder_binding = compute_holder_binding( - env, - holder, - &commitment, - &nullifier, - &final_challenge, - ); + let holder_binding = + compute_holder_binding(env, holder, &commitment, &nullifier, &final_challenge); ZkProof { credential_commitment: commitment, @@ -207,7 +208,10 @@ fn test_zk_proof_invalid_challenge_rejected() { ); // Tamper with challenge hash - proof.challenge = env.crypto().sha256(&Bytes::from_slice(&env, b"tampered")).into(); + proof.challenge = env + .crypto() + .sha256(&Bytes::from_slice(&env, b"tampered")) + .into(); // Update holder_binding so it passes step 1, but fails challenge step proof.holder_binding = compute_holder_binding( &env,