1pub use chio_core_types::{canonical_json_bytes, capability, crypto, receipt};
2pub use chio_governance as governance;
3pub use chio_listing as listing;
4
5pub mod bidding;
6pub use bidding::{
7 accept, bid, AcceptedBid, AskResponse, BidMintContext, BidRequest, BiddingError,
8 RequestedScope, SignedAcceptedBid, SignedAskResponse, SignedBidRequest, ACCEPTED_BID_SCHEMA,
9 ASK_RESPONSE_SCHEMA, BID_REQUEST_SCHEMA,
10};
11
12use serde::{Deserialize, Serialize};
13
14use crate::capability::MonetaryAmount;
15use crate::crypto::sha256_hex;
16use crate::governance::{
17 GenericGovernanceCaseKind, GenericGovernanceCaseState, SignedGenericGovernanceCase,
18 SignedGenericGovernanceCharter,
19};
20use crate::listing::{
21 normalize_namespace, GenericListingActorKind, GenericRegistryPublisher,
22 GenericTrustAdmissionClass, SignedGenericListing, SignedGenericTrustActivation,
23};
24use crate::receipt::SignedExportEnvelope;
25
26pub const OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA: &str = "chio.registry.market-fee-schedule.v1";
27pub const OPEN_MARKET_PENALTY_ARTIFACT_SCHEMA: &str = "chio.registry.market-penalty.v1";
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum OpenMarketBondClass {
32 Publication,
33 Listing,
34 Dispute,
35}
36
37#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
38#[serde(rename_all = "snake_case")]
39pub enum OpenMarketCollateralReferenceKind {
40 CreditBond,
41 ExternalReference,
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "snake_case")]
46pub enum OpenMarketAbuseClass {
47 SpamPublication,
48 FraudulentListing,
49 ReplayPublication,
50 UnverifiableListingBehavior,
51}
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum OpenMarketPenaltyAction {
56 HoldBond,
57 SlashBond,
58 ReverseSlash,
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "snake_case")]
63pub enum OpenMarketPenaltyState {
64 Proposed,
65 Enforced,
66 Reversed,
67 Denied,
68 Superseded,
69}
70
71#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "snake_case")]
73pub enum OpenMarketPenaltyEffectiveState {
74 Clear,
75 BondHeld,
76 BondSlashed,
77 Reversed,
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(rename_all = "snake_case")]
82pub enum OpenMarketEvidenceKind {
83 GovernanceCase,
84 TrustActivation,
85 Listing,
86 PortableNegativeEvent,
87 External,
88}
89
90#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
91#[serde(rename_all = "snake_case")]
92pub enum OpenMarketFindingCode {
93 ListingUnverifiable,
94 FeeScheduleUnverifiable,
95 FeeScheduleExpired,
96 FeeScheduleScopeMismatch,
97 ActivationUnverifiable,
98 ActivationMissing,
99 ActivationMismatch,
100 GovernanceCaseAuthorityInvalid,
101 GovernanceCaseExpired,
102 GovernanceCaseKindInvalid,
103 PenaltyUnverifiable,
104 PenaltyExpired,
105 BondRequirementMissing,
106 BondRequirementNotSlashable,
107 PenaltyCurrencyMismatch,
108 PenaltyAmountExceedsBond,
109 PriorPenaltyMissing,
110 PriorPenaltyInvalid,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "camelCase")]
115pub struct OpenMarketEconomicsScope {
116 pub namespace: String,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub allowed_listing_operator_ids: Vec<String>,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub allowed_actor_kinds: Vec<GenericListingActorKind>,
121 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub allowed_admission_classes: Vec<GenericTrustAdmissionClass>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub policy_reference: Option<String>,
125}
126
127impl OpenMarketEconomicsScope {
128 pub fn validate(&self) -> Result<(), String> {
129 validate_non_empty(&self.namespace, "scope.namespace")?;
130 for (index, operator_id) in self.allowed_listing_operator_ids.iter().enumerate() {
131 validate_non_empty(
132 operator_id,
133 &format!("scope.allowed_listing_operator_ids[{index}]"),
134 )?;
135 }
136 Ok(())
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "camelCase")]
142pub struct OpenMarketBondRequirement {
143 pub bond_class: OpenMarketBondClass,
144 pub required_amount: MonetaryAmount,
145 pub collateral_reference_kind: OpenMarketCollateralReferenceKind,
146 pub slashable: bool,
147}
148
149impl OpenMarketBondRequirement {
150 pub fn validate(&self, field: &str) -> Result<(), String> {
151 validate_monetary_amount(&self.required_amount, &format!("{field}.required_amount"))
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156#[serde(rename_all = "camelCase")]
157pub struct OpenMarketFeeScheduleArtifact {
158 pub schema: String,
159 pub fee_schedule_id: String,
160 pub namespace: String,
161 pub governing_operator_id: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub governing_operator_name: Option<String>,
164 pub scope: OpenMarketEconomicsScope,
165 pub publication_fee: MonetaryAmount,
166 pub dispute_fee: MonetaryAmount,
167 pub market_participation_fee: MonetaryAmount,
168 pub bond_requirements: Vec<OpenMarketBondRequirement>,
169 pub issued_at: u64,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub expires_at: Option<u64>,
172 pub issued_by: String,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub note: Option<String>,
175}
176
177impl OpenMarketFeeScheduleArtifact {
178 pub fn validate(&self) -> Result<(), String> {
179 if self.schema != OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA {
180 return Err(format!(
181 "unsupported open-market fee schedule schema: {}",
182 self.schema
183 ));
184 }
185 validate_non_empty(&self.fee_schedule_id, "fee_schedule_id")?;
186 validate_non_empty(&self.namespace, "namespace")?;
187 validate_non_empty(&self.governing_operator_id, "governing_operator_id")?;
188 validate_non_empty(&self.issued_by, "issued_by")?;
189 self.scope.validate()?;
190 if normalize_namespace(&self.namespace) != normalize_namespace(&self.scope.namespace) {
191 return Err("fee schedule namespace must match scope namespace".to_string());
192 }
193 validate_monetary_amount(&self.publication_fee, "publication_fee")?;
194 validate_monetary_amount(&self.dispute_fee, "dispute_fee")?;
195 validate_monetary_amount(&self.market_participation_fee, "market_participation_fee")?;
196 if self.bond_requirements.is_empty() {
197 return Err("bond_requirements must not be empty".to_string());
198 }
199 for (index, requirement) in self.bond_requirements.iter().enumerate() {
200 requirement.validate(&format!("bond_requirements[{index}]"))?;
201 }
202 if let Some(expires_at) = self.expires_at {
203 if expires_at <= self.issued_at {
204 return Err("expires_at must be greater than issued_at".to_string());
205 }
206 }
207 Ok(())
208 }
209}
210
211pub type SignedOpenMarketFeeSchedule = SignedExportEnvelope<OpenMarketFeeScheduleArtifact>;
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214#[serde(rename_all = "camelCase")]
215pub struct OpenMarketFeeScheduleIssueRequest {
216 pub scope: OpenMarketEconomicsScope,
217 pub publication_fee: MonetaryAmount,
218 pub dispute_fee: MonetaryAmount,
219 pub market_participation_fee: MonetaryAmount,
220 pub bond_requirements: Vec<OpenMarketBondRequirement>,
221 pub issued_by: String,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub issued_at: Option<u64>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub expires_at: Option<u64>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub note: Option<String>,
228}
229
230impl OpenMarketFeeScheduleIssueRequest {
231 pub fn validate(&self) -> Result<(), String> {
232 self.scope.validate()?;
233 validate_non_empty(&self.issued_by, "issued_by")?;
234 validate_monetary_amount(&self.publication_fee, "publication_fee")?;
235 validate_monetary_amount(&self.dispute_fee, "dispute_fee")?;
236 validate_monetary_amount(&self.market_participation_fee, "market_participation_fee")?;
237 if self.bond_requirements.is_empty() {
238 return Err("bond_requirements must not be empty".to_string());
239 }
240 for (index, requirement) in self.bond_requirements.iter().enumerate() {
241 requirement.validate(&format!("bond_requirements[{index}]"))?;
242 }
243 Ok(())
244 }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "camelCase")]
249pub struct OpenMarketEvidenceReference {
250 pub kind: OpenMarketEvidenceKind,
251 pub reference_id: String,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub uri: Option<String>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub sha256: Option<String>,
256}
257
258impl OpenMarketEvidenceReference {
259 pub fn validate(&self, field: &str) -> Result<(), String> {
260 validate_non_empty(&self.reference_id, &format!("{field}.reference_id"))?;
261 if let Some(uri) = self.uri.as_deref() {
262 validate_non_empty(uri, &format!("{field}.uri"))?;
263 }
264 Ok(())
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
269#[serde(rename_all = "camelCase")]
270pub struct OpenMarketPenaltyArtifact {
271 pub schema: String,
272 pub penalty_id: String,
273 pub fee_schedule_id: String,
274 pub charter_id: String,
275 pub case_id: String,
276 pub governing_operator_id: String,
277 pub namespace: String,
278 pub listing_id: String,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub activation_id: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub subject_operator_id: Option<String>,
283 pub abuse_class: OpenMarketAbuseClass,
284 pub bond_class: OpenMarketBondClass,
285 pub action: OpenMarketPenaltyAction,
286 pub state: OpenMarketPenaltyState,
287 pub penalty_amount: MonetaryAmount,
288 pub opened_at: u64,
289 pub updated_at: u64,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub expires_at: Option<u64>,
292 pub evidence_refs: Vec<OpenMarketEvidenceReference>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub supersedes_penalty_id: Option<String>,
295 pub issued_by: String,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub note: Option<String>,
298}
299
300impl OpenMarketPenaltyArtifact {
301 pub fn validate(&self) -> Result<(), String> {
302 if self.schema != OPEN_MARKET_PENALTY_ARTIFACT_SCHEMA {
303 return Err(format!(
304 "unsupported open-market penalty schema: {}",
305 self.schema
306 ));
307 }
308 validate_non_empty(&self.penalty_id, "penalty_id")?;
309 validate_non_empty(&self.fee_schedule_id, "fee_schedule_id")?;
310 validate_non_empty(&self.charter_id, "charter_id")?;
311 validate_non_empty(&self.case_id, "case_id")?;
312 validate_non_empty(&self.governing_operator_id, "governing_operator_id")?;
313 validate_non_empty(&self.namespace, "namespace")?;
314 validate_non_empty(&self.listing_id, "listing_id")?;
315 validate_non_empty(&self.issued_by, "issued_by")?;
316 validate_monetary_amount(&self.penalty_amount, "penalty_amount")?;
317 if self.updated_at < self.opened_at {
318 return Err("updated_at must be greater than or equal to opened_at".to_string());
319 }
320 if let Some(expires_at) = self.expires_at {
321 if expires_at <= self.opened_at {
322 return Err("expires_at must be greater than opened_at".to_string());
323 }
324 }
325 if self.evidence_refs.is_empty() {
326 return Err("evidence_refs must not be empty".to_string());
327 }
328 for (index, evidence_ref) in self.evidence_refs.iter().enumerate() {
329 evidence_ref.validate(&format!("evidence_refs[{index}]"))?;
330 }
331 if matches!(self.action, OpenMarketPenaltyAction::ReverseSlash) {
332 if self.supersedes_penalty_id.as_deref().is_none() {
333 return Err("reverse_slash penalty requires supersedes_penalty_id".to_string());
334 }
335 if !matches!(self.state, OpenMarketPenaltyState::Reversed) {
336 return Err("reverse_slash penalty must use reversed state".to_string());
337 }
338 }
339 Ok(())
340 }
341}
342
343pub type SignedOpenMarketPenalty = SignedExportEnvelope<OpenMarketPenaltyArtifact>;
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
346#[serde(rename_all = "camelCase")]
347pub struct OpenMarketPenaltyIssueRequest {
348 pub fee_schedule: SignedOpenMarketFeeSchedule,
349 pub charter: SignedGenericGovernanceCharter,
350 pub case: SignedGenericGovernanceCase,
351 pub listing: SignedGenericListing,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub activation: Option<SignedGenericTrustActivation>,
354 pub abuse_class: OpenMarketAbuseClass,
355 pub bond_class: OpenMarketBondClass,
356 pub action: OpenMarketPenaltyAction,
357 pub state: OpenMarketPenaltyState,
358 pub penalty_amount: MonetaryAmount,
359 pub evidence_refs: Vec<OpenMarketEvidenceReference>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub subject_operator_id: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub supersedes_penalty_id: Option<String>,
364 pub issued_by: String,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub opened_at: Option<u64>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub updated_at: Option<u64>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub expires_at: Option<u64>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub note: Option<String>,
373}
374
375impl OpenMarketPenaltyIssueRequest {
376 pub fn validate(&self) -> Result<(), String> {
377 verify_signed_listing(&self.listing, "penalty listing")?;
378 verify_signed_fee_schedule(&self.fee_schedule)?;
379 verify_signed_charter(&self.charter)?;
380 verify_signed_case(&self.case)?;
381 if let Some(activation) = self.activation.as_ref() {
382 verify_signed_activation(activation)?;
383 }
384 validate_non_empty(&self.issued_by, "issued_by")?;
385 validate_monetary_amount(&self.penalty_amount, "penalty_amount")?;
386 if self.evidence_refs.is_empty() {
387 return Err("evidence_refs must not be empty".to_string());
388 }
389 for (index, evidence_ref) in self.evidence_refs.iter().enumerate() {
390 evidence_ref.validate(&format!("evidence_refs[{index}]"))?;
391 }
392 Ok(())
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
397#[serde(rename_all = "camelCase")]
398pub struct OpenMarketPenaltyEvaluationRequest {
399 pub fee_schedule: SignedOpenMarketFeeSchedule,
400 pub listing: SignedGenericListing,
401 pub current_publisher: GenericRegistryPublisher,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub activation: Option<SignedGenericTrustActivation>,
404 pub charter: SignedGenericGovernanceCharter,
405 pub case: SignedGenericGovernanceCase,
406 pub penalty: SignedOpenMarketPenalty,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub prior_penalty: Option<SignedOpenMarketPenalty>,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub evaluated_at: Option<u64>,
411}
412
413impl OpenMarketPenaltyEvaluationRequest {
414 pub fn validate(&self) -> Result<(), String> {
415 self.listing.body.validate()?;
416 self.current_publisher.validate()?;
417 Ok(())
418 }
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
422#[serde(rename_all = "camelCase")]
423pub struct OpenMarketFinding {
424 pub code: OpenMarketFindingCode,
425 pub message: String,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
429#[serde(rename_all = "camelCase")]
430pub struct OpenMarketPenaltyEvaluation {
431 pub listing_id: String,
432 pub namespace: String,
433 pub fee_schedule_id: String,
434 pub charter_id: String,
435 pub case_id: String,
436 pub penalty_id: String,
437 pub governing_operator_id: String,
438 pub action: OpenMarketPenaltyAction,
439 pub state: OpenMarketPenaltyState,
440 pub effective_state: OpenMarketPenaltyEffectiveState,
441 pub evaluated_at: u64,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub publication_fee: Option<MonetaryAmount>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub dispute_fee: Option<MonetaryAmount>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub market_participation_fee: Option<MonetaryAmount>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub bond_requirement: Option<OpenMarketBondRequirement>,
450 pub blocks_admission: bool,
451 #[serde(default, skip_serializing_if = "Vec::is_empty")]
452 pub findings: Vec<OpenMarketFinding>,
453}
454
455pub fn build_open_market_fee_schedule_artifact(
456 local_operator_id: &str,
457 local_operator_name: Option<String>,
458 request: &OpenMarketFeeScheduleIssueRequest,
459 issued_at: u64,
460) -> Result<OpenMarketFeeScheduleArtifact, String> {
461 request.validate()?;
462 validate_non_empty(local_operator_id, "local_operator_id")?;
463 let issued_at = request.issued_at.unwrap_or(issued_at);
464 let fee_schedule_id = format!(
465 "market-fee-schedule-{}",
466 sha256_hex(
467 &canonical_json_bytes(&(
468 local_operator_id,
469 normalize_namespace(&request.scope.namespace),
470 &request.publication_fee,
471 &request.dispute_fee,
472 &request.market_participation_fee,
473 &request.bond_requirements,
474 issued_at,
475 ))
476 .map_err(|error| error.to_string())?
477 )
478 );
479 let artifact = OpenMarketFeeScheduleArtifact {
480 schema: OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA.to_string(),
481 fee_schedule_id,
482 namespace: request.scope.namespace.clone(),
483 governing_operator_id: local_operator_id.to_string(),
484 governing_operator_name: local_operator_name,
485 scope: request.scope.clone(),
486 publication_fee: request.publication_fee.clone(),
487 dispute_fee: request.dispute_fee.clone(),
488 market_participation_fee: request.market_participation_fee.clone(),
489 bond_requirements: request.bond_requirements.clone(),
490 issued_at,
491 expires_at: request.expires_at,
492 issued_by: request.issued_by.clone(),
493 note: request.note.clone(),
494 };
495 artifact.validate()?;
496 Ok(artifact)
497}
498
499pub fn build_open_market_penalty_artifact(
500 local_operator_id: &str,
501 request: &OpenMarketPenaltyIssueRequest,
502 issued_at: u64,
503) -> Result<OpenMarketPenaltyArtifact, String> {
504 request.validate()?;
505 validate_non_empty(local_operator_id, "local_operator_id")?;
506 if request.fee_schedule.body.governing_operator_id != local_operator_id
507 || request.charter.body.governing_operator_id != local_operator_id
508 || request.case.body.governing_operator_id != local_operator_id
509 {
510 return Err(
511 "open-market penalty must be issued by the fee schedule and governance authority operator"
512 .to_string(),
513 );
514 }
515 if request
516 .activation
517 .as_ref()
518 .is_some_and(|activation| activation.body.local_operator_id != local_operator_id)
519 {
520 return Err(
521 "open-market penalties must use a trust activation issued by the governing operator"
522 .to_string(),
523 );
524 }
525 let opened_at = request.opened_at.unwrap_or(issued_at);
526 let updated_at = request.updated_at.unwrap_or(opened_at);
527 let penalty_id = format!(
528 "market-penalty-{}",
529 sha256_hex(
530 &canonical_json_bytes(&(
531 local_operator_id,
532 &request.fee_schedule.body.fee_schedule_id,
533 &request.case.body.case_id,
534 &request.listing.body.listing_id,
535 request.bond_class,
536 request.action,
537 request.state,
538 opened_at,
539 &request.supersedes_penalty_id,
540 ))
541 .map_err(|error| error.to_string())?
542 )
543 );
544 let artifact = OpenMarketPenaltyArtifact {
545 schema: OPEN_MARKET_PENALTY_ARTIFACT_SCHEMA.to_string(),
546 penalty_id,
547 fee_schedule_id: request.fee_schedule.body.fee_schedule_id.clone(),
548 charter_id: request.charter.body.charter_id.clone(),
549 case_id: request.case.body.case_id.clone(),
550 governing_operator_id: local_operator_id.to_string(),
551 namespace: request.listing.body.namespace.clone(),
552 listing_id: request.listing.body.listing_id.clone(),
553 activation_id: request
554 .activation
555 .as_ref()
556 .map(|activation| activation.body.activation_id.clone()),
557 subject_operator_id: request.subject_operator_id.clone(),
558 abuse_class: request.abuse_class,
559 bond_class: request.bond_class,
560 action: request.action,
561 state: request.state,
562 penalty_amount: request.penalty_amount.clone(),
563 opened_at,
564 updated_at,
565 expires_at: request.expires_at,
566 evidence_refs: request.evidence_refs.clone(),
567 supersedes_penalty_id: request.supersedes_penalty_id.clone(),
568 issued_by: request.issued_by.clone(),
569 note: request.note.clone(),
570 };
571 artifact.validate()?;
572 Ok(artifact)
573}
574
575pub fn evaluate_open_market_penalty(
576 request: &OpenMarketPenaltyEvaluationRequest,
577 now: u64,
578) -> Result<OpenMarketPenaltyEvaluation, String> {
579 request.validate()?;
580 let evaluated_at = request.evaluated_at.unwrap_or(now);
581
582 if let Err(error) = verify_signed_listing(&request.listing, "penalty listing") {
583 return Ok(open_market_failure(
584 request,
585 evaluated_at,
586 OpenMarketFindingCode::ListingUnverifiable,
587 &error,
588 None,
589 ));
590 }
591 if let Err(error) = verify_signed_fee_schedule(&request.fee_schedule) {
592 return Ok(open_market_failure(
593 request,
594 evaluated_at,
595 OpenMarketFindingCode::FeeScheduleUnverifiable,
596 &error,
597 None,
598 ));
599 }
600 if let Some(activation) = request.activation.as_ref() {
601 if let Err(error) = verify_signed_activation(activation) {
602 return Ok(open_market_failure(
603 request,
604 evaluated_at,
605 OpenMarketFindingCode::ActivationUnverifiable,
606 &error,
607 Some(&request.fee_schedule.body),
608 ));
609 }
610 }
611 if let Err(error) = verify_signed_charter(&request.charter) {
612 return Ok(open_market_failure(
613 request,
614 evaluated_at,
615 OpenMarketFindingCode::GovernanceCaseAuthorityInvalid,
616 &error,
617 Some(&request.fee_schedule.body),
618 ));
619 }
620 if let Err(error) = verify_signed_case(&request.case) {
621 return Ok(open_market_failure(
622 request,
623 evaluated_at,
624 OpenMarketFindingCode::GovernanceCaseAuthorityInvalid,
625 &error,
626 Some(&request.fee_schedule.body),
627 ));
628 }
629 if let Err(error) = verify_signed_penalty(&request.penalty) {
630 return Ok(open_market_failure(
631 request,
632 evaluated_at,
633 OpenMarketFindingCode::PenaltyUnverifiable,
634 &error,
635 Some(&request.fee_schedule.body),
636 ));
637 }
638 if let Some(prior_penalty) = request.prior_penalty.as_ref() {
639 if let Err(error) = verify_signed_penalty(prior_penalty) {
640 return Ok(open_market_failure(
641 request,
642 evaluated_at,
643 OpenMarketFindingCode::PriorPenaltyInvalid,
644 &error,
645 Some(&request.fee_schedule.body),
646 ));
647 }
648 }
649
650 let listing = &request.listing.body;
651 let fee_schedule = &request.fee_schedule.body;
652 let charter = &request.charter.body;
653 let governance_case = &request.case.body;
654 let penalty = &request.penalty.body;
655 let namespace = normalize_namespace(&listing.namespace);
656
657 if let Some(activation) = request.activation.as_ref() {
658 if activation.body.local_operator_id != fee_schedule.governing_operator_id {
659 return Ok(open_market_failure(
660 request,
661 evaluated_at,
662 OpenMarketFindingCode::ActivationMismatch,
663 "open-market penalties require a trust activation issued by the governing operator",
664 Some(fee_schedule),
665 ));
666 }
667 }
668
669 if normalize_namespace(&fee_schedule.namespace) != namespace
670 || normalize_namespace(&fee_schedule.scope.namespace) != namespace
671 {
672 return Ok(open_market_failure(
673 request,
674 evaluated_at,
675 OpenMarketFindingCode::FeeScheduleScopeMismatch,
676 "fee schedule namespace does not match the current listing namespace",
677 Some(fee_schedule),
678 ));
679 }
680 if normalize_namespace(&charter.authority_scope.namespace) != namespace
681 || normalize_namespace(&governance_case.namespace) != namespace
682 || normalize_namespace(&penalty.namespace) != namespace
683 || governance_case.listing_id != listing.listing_id
684 || penalty.listing_id != listing.listing_id
685 || penalty.case_id != governance_case.case_id
686 || penalty.charter_id != charter.charter_id
687 || penalty.fee_schedule_id != fee_schedule.fee_schedule_id
688 {
689 return Ok(open_market_failure(
690 request,
691 evaluated_at,
692 OpenMarketFindingCode::GovernanceCaseAuthorityInvalid,
693 "governance or penalty authority does not match the current listing, namespace, or fee schedule",
694 Some(fee_schedule),
695 ));
696 }
697 if fee_schedule.governing_operator_id != charter.governing_operator_id
698 || fee_schedule.governing_operator_id != governance_case.governing_operator_id
699 || fee_schedule.governing_operator_id != penalty.governing_operator_id
700 {
701 return Ok(open_market_failure(
702 request,
703 evaluated_at,
704 OpenMarketFindingCode::GovernanceCaseAuthorityInvalid,
705 "fee schedule, governance, and penalty operators must match",
706 Some(fee_schedule),
707 ));
708 }
709
710 if fee_schedule
711 .expires_at
712 .is_some_and(|expires_at| expires_at <= evaluated_at)
713 {
714 return Ok(open_market_failure(
715 request,
716 evaluated_at,
717 OpenMarketFindingCode::FeeScheduleExpired,
718 "open-market fee schedule has expired",
719 Some(fee_schedule),
720 ));
721 }
722 if charter
723 .expires_at
724 .is_some_and(|expires_at| expires_at <= evaluated_at)
725 || governance_case
726 .expires_at
727 .is_some_and(|expires_at| expires_at <= evaluated_at)
728 {
729 return Ok(open_market_failure(
730 request,
731 evaluated_at,
732 OpenMarketFindingCode::GovernanceCaseExpired,
733 "governance authority has expired",
734 Some(fee_schedule),
735 ));
736 }
737 if penalty
738 .expires_at
739 .is_some_and(|expires_at| expires_at <= evaluated_at)
740 {
741 return Ok(open_market_failure(
742 request,
743 evaluated_at,
744 OpenMarketFindingCode::PenaltyExpired,
745 "open-market penalty has expired",
746 Some(fee_schedule),
747 ));
748 }
749 if !fee_schedule.scope.allowed_listing_operator_ids.is_empty()
750 && !fee_schedule
751 .scope
752 .allowed_listing_operator_ids
753 .contains(&request.current_publisher.operator_id)
754 {
755 return Ok(open_market_failure(
756 request,
757 evaluated_at,
758 OpenMarketFindingCode::FeeScheduleScopeMismatch,
759 "current listing publisher falls outside the fee schedule scope",
760 Some(fee_schedule),
761 ));
762 }
763 if !fee_schedule.scope.allowed_actor_kinds.is_empty()
764 && !fee_schedule
765 .scope
766 .allowed_actor_kinds
767 .contains(&listing.subject.actor_kind)
768 {
769 return Ok(open_market_failure(
770 request,
771 evaluated_at,
772 OpenMarketFindingCode::FeeScheduleScopeMismatch,
773 "listing actor kind falls outside the fee schedule scope",
774 Some(fee_schedule),
775 ));
776 }
777 if !fee_schedule.scope.allowed_admission_classes.is_empty() {
778 let Some(activation) = request.activation.as_ref() else {
779 return Ok(open_market_failure(
780 request,
781 evaluated_at,
782 OpenMarketFindingCode::ActivationMissing,
783 "fee schedule requires an explicit trust activation class",
784 Some(fee_schedule),
785 ));
786 };
787 if governance_case.activation_id.as_deref() != Some(activation.body.activation_id.as_str())
788 || penalty.activation_id.as_deref() != Some(activation.body.activation_id.as_str())
789 {
790 return Ok(open_market_failure(
791 request,
792 evaluated_at,
793 OpenMarketFindingCode::ActivationMismatch,
794 "governance case or penalty activation does not match the current trust activation",
795 Some(fee_schedule),
796 ));
797 }
798 if !fee_schedule
799 .scope
800 .allowed_admission_classes
801 .contains(&activation.body.admission_class)
802 {
803 return Ok(open_market_failure(
804 request,
805 evaluated_at,
806 OpenMarketFindingCode::ActivationMismatch,
807 "trust activation admission class falls outside the fee schedule scope",
808 Some(fee_schedule),
809 ));
810 }
811 }
812
813 let bond_requirement = fee_schedule
814 .bond_requirements
815 .iter()
816 .find(|requirement| requirement.bond_class == penalty.bond_class)
817 .cloned();
818 let Some(bond_requirement) = bond_requirement else {
819 return Ok(open_market_failure(
820 request,
821 evaluated_at,
822 OpenMarketFindingCode::BondRequirementMissing,
823 "fee schedule does not define the required bond class for this penalty",
824 Some(fee_schedule),
825 ));
826 };
827
828 match penalty.action {
829 OpenMarketPenaltyAction::HoldBond | OpenMarketPenaltyAction::SlashBond => {
830 if !matches!(
831 (governance_case.kind, governance_case.state),
832 (
833 GenericGovernanceCaseKind::Sanction,
834 GenericGovernanceCaseState::Enforced
835 )
836 ) {
837 return Ok(open_market_failure(
838 request,
839 evaluated_at,
840 OpenMarketFindingCode::GovernanceCaseKindInvalid,
841 "bond hold or slash requires an enforced sanction case",
842 Some(fee_schedule),
843 ));
844 }
845 if matches!(penalty.action, OpenMarketPenaltyAction::SlashBond)
846 && !bond_requirement.slashable
847 {
848 return Ok(open_market_failure(
849 request,
850 evaluated_at,
851 OpenMarketFindingCode::BondRequirementNotSlashable,
852 "selected bond requirement is not slashable",
853 Some(fee_schedule),
854 ));
855 }
856 }
857 OpenMarketPenaltyAction::ReverseSlash => {
858 if !matches!(governance_case.kind, GenericGovernanceCaseKind::Appeal) {
859 return Ok(open_market_failure(
860 request,
861 evaluated_at,
862 OpenMarketFindingCode::GovernanceCaseKindInvalid,
863 "reverse slash requires an appeal governance case",
864 Some(fee_schedule),
865 ));
866 }
867 let Some(prior_penalty) = request.prior_penalty.as_ref() else {
868 return Ok(open_market_failure(
869 request,
870 evaluated_at,
871 OpenMarketFindingCode::PriorPenaltyMissing,
872 "reverse slash requires prior_penalty",
873 Some(fee_schedule),
874 ));
875 };
876 let Some(supersedes_penalty_id) = penalty.supersedes_penalty_id.as_deref() else {
877 return Ok(open_market_failure(
878 request,
879 evaluated_at,
880 OpenMarketFindingCode::PriorPenaltyInvalid,
881 "reverse slash must reference the prior penalty id",
882 Some(fee_schedule),
883 ));
884 };
885 if prior_penalty.body.penalty_id != supersedes_penalty_id
886 || prior_penalty.body.listing_id != listing.listing_id
887 || prior_penalty.body.fee_schedule_id != fee_schedule.fee_schedule_id
888 || prior_penalty.body.bond_class != penalty.bond_class
889 || !matches!(
890 prior_penalty.body.action,
891 OpenMarketPenaltyAction::HoldBond | OpenMarketPenaltyAction::SlashBond
892 )
893 || !matches!(prior_penalty.body.state, OpenMarketPenaltyState::Enforced)
894 {
895 return Ok(open_market_failure(
896 request,
897 evaluated_at,
898 OpenMarketFindingCode::PriorPenaltyInvalid,
899 "prior penalty does not match the reverse-slash target",
900 Some(fee_schedule),
901 ));
902 }
903 }
904 }
905
906 if bond_requirement.required_amount.currency != penalty.penalty_amount.currency {
907 return Ok(open_market_failure(
908 request,
909 evaluated_at,
910 OpenMarketFindingCode::PenaltyCurrencyMismatch,
911 "penalty currency must match the configured bond currency",
912 Some(fee_schedule),
913 ));
914 }
915 if penalty.penalty_amount.units > bond_requirement.required_amount.units {
916 return Ok(open_market_failure(
917 request,
918 evaluated_at,
919 OpenMarketFindingCode::PenaltyAmountExceedsBond,
920 "penalty amount exceeds the configured bond requirement",
921 Some(fee_schedule),
922 ));
923 }
924
925 let (effective_state, blocks_admission) =
926 open_market_effective_state(penalty.action, penalty.state);
927
928 Ok(OpenMarketPenaltyEvaluation {
929 listing_id: listing.listing_id.clone(),
930 namespace,
931 fee_schedule_id: fee_schedule.fee_schedule_id.clone(),
932 charter_id: charter.charter_id.clone(),
933 case_id: governance_case.case_id.clone(),
934 penalty_id: penalty.penalty_id.clone(),
935 governing_operator_id: penalty.governing_operator_id.clone(),
936 action: penalty.action,
937 state: penalty.state,
938 effective_state,
939 evaluated_at,
940 publication_fee: Some(fee_schedule.publication_fee.clone()),
941 dispute_fee: Some(fee_schedule.dispute_fee.clone()),
942 market_participation_fee: Some(fee_schedule.market_participation_fee.clone()),
943 bond_requirement: Some(bond_requirement),
944 blocks_admission,
945 findings: Vec::new(),
946 })
947}
948
949fn open_market_effective_state(
950 action: OpenMarketPenaltyAction,
951 state: OpenMarketPenaltyState,
952) -> (OpenMarketPenaltyEffectiveState, bool) {
953 match state {
954 OpenMarketPenaltyState::Proposed
955 | OpenMarketPenaltyState::Denied
956 | OpenMarketPenaltyState::Superseded => (OpenMarketPenaltyEffectiveState::Clear, false),
957 OpenMarketPenaltyState::Reversed => (OpenMarketPenaltyEffectiveState::Reversed, false),
958 OpenMarketPenaltyState::Enforced => match action {
959 OpenMarketPenaltyAction::HoldBond => (OpenMarketPenaltyEffectiveState::BondHeld, true),
960 OpenMarketPenaltyAction::SlashBond => {
961 (OpenMarketPenaltyEffectiveState::BondSlashed, true)
962 }
963 OpenMarketPenaltyAction::ReverseSlash => {
964 (OpenMarketPenaltyEffectiveState::Reversed, false)
965 }
966 },
967 }
968}
969
970fn open_market_failure(
971 request: &OpenMarketPenaltyEvaluationRequest,
972 evaluated_at: u64,
973 code: OpenMarketFindingCode,
974 message: &str,
975 fee_schedule: Option<&OpenMarketFeeScheduleArtifact>,
976) -> OpenMarketPenaltyEvaluation {
977 OpenMarketPenaltyEvaluation {
978 listing_id: request.listing.body.listing_id.clone(),
979 namespace: request.listing.body.namespace.clone(),
980 fee_schedule_id: request.penalty.body.fee_schedule_id.clone(),
981 charter_id: request.penalty.body.charter_id.clone(),
982 case_id: request.penalty.body.case_id.clone(),
983 penalty_id: request.penalty.body.penalty_id.clone(),
984 governing_operator_id: request.penalty.body.governing_operator_id.clone(),
985 action: request.penalty.body.action,
986 state: request.penalty.body.state,
987 effective_state: OpenMarketPenaltyEffectiveState::Clear,
988 evaluated_at,
989 publication_fee: fee_schedule.map(|schedule| schedule.publication_fee.clone()),
990 dispute_fee: fee_schedule.map(|schedule| schedule.dispute_fee.clone()),
991 market_participation_fee: fee_schedule
992 .map(|schedule| schedule.market_participation_fee.clone()),
993 bond_requirement: None,
994 blocks_admission: false,
995 findings: vec![OpenMarketFinding {
996 code,
997 message: message.to_string(),
998 }],
999 }
1000}
1001
1002fn verify_signed_listing(listing: &SignedGenericListing, label: &str) -> Result<(), String> {
1003 listing.body.validate()?;
1004 if !listing
1005 .verify_signature()
1006 .map_err(|error| error.to_string())?
1007 {
1008 return Err(format!("{label} signature is invalid"));
1009 }
1010 Ok(())
1011}
1012
1013fn verify_signed_activation(activation: &SignedGenericTrustActivation) -> Result<(), String> {
1014 activation.body.validate()?;
1015 if !activation
1016 .verify_signature()
1017 .map_err(|error| error.to_string())?
1018 {
1019 return Err("trust activation signature is invalid".to_string());
1020 }
1021 Ok(())
1022}
1023
1024fn verify_signed_charter(charter: &SignedGenericGovernanceCharter) -> Result<(), String> {
1025 charter.body.validate()?;
1026 if !charter
1027 .verify_signature()
1028 .map_err(|error| error.to_string())?
1029 {
1030 return Err("governance charter signature is invalid".to_string());
1031 }
1032 Ok(())
1033}
1034
1035fn verify_signed_case(case: &SignedGenericGovernanceCase) -> Result<(), String> {
1036 case.body.validate()?;
1037 if !case.verify_signature().map_err(|error| error.to_string())? {
1038 return Err("governance case signature is invalid".to_string());
1039 }
1040 Ok(())
1041}
1042
1043fn verify_signed_fee_schedule(schedule: &SignedOpenMarketFeeSchedule) -> Result<(), String> {
1044 schedule.body.validate()?;
1045 if !schedule
1046 .verify_signature()
1047 .map_err(|error| error.to_string())?
1048 {
1049 return Err("fee schedule signature is invalid".to_string());
1050 }
1051 Ok(())
1052}
1053
1054fn verify_signed_penalty(penalty: &SignedOpenMarketPenalty) -> Result<(), String> {
1055 penalty.body.validate()?;
1056 if !penalty
1057 .verify_signature()
1058 .map_err(|error| error.to_string())?
1059 {
1060 return Err("penalty signature is invalid".to_string());
1061 }
1062 Ok(())
1063}
1064
1065fn validate_monetary_amount(value: &MonetaryAmount, field: &str) -> Result<(), String> {
1066 if value.units == 0 {
1067 return Err(format!("{field}.units must be greater than zero"));
1068 }
1069 validate_non_empty(&value.currency, &format!("{field}.currency"))
1070}
1071
1072fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
1073 if value.trim().is_empty() {
1074 Err(format!("{field} must not be empty"))
1075 } else {
1076 Ok(())
1077 }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use crate::crypto::Keypair;
1084 use crate::governance::{
1085 build_generic_governance_case_artifact, build_generic_governance_charter_artifact,
1086 GenericGovernanceAuthorityScope, GenericGovernanceCaseIssueRequest,
1087 GenericGovernanceCharterIssueRequest, GenericGovernanceEvidenceKind,
1088 GenericGovernanceEvidenceReference,
1089 };
1090 use crate::listing::{
1091 build_generic_trust_activation_artifact, GenericListingArtifact, GenericListingBoundary,
1092 GenericListingCompatibilityReference, GenericListingFreshnessState,
1093 GenericListingReplicaFreshness, GenericListingStatus, GenericListingSubject,
1094 GenericNamespaceArtifact, GenericNamespaceLifecycleState, GenericNamespaceOwnership,
1095 GenericRegistryPublisher, GenericRegistryPublisherRole, GenericTrustActivationDisposition,
1096 GenericTrustActivationEligibility, GenericTrustActivationIssueRequest,
1097 GenericTrustActivationReviewContext, GENERIC_LISTING_ARTIFACT_SCHEMA,
1098 GENERIC_NAMESPACE_ARTIFACT_SCHEMA,
1099 };
1100
1101 fn sample_listing(owner_id: &str, signing_keypair: &Keypair) -> SignedGenericListing {
1102 let namespace = GenericNamespaceArtifact {
1103 schema: GENERIC_NAMESPACE_ARTIFACT_SCHEMA.to_string(),
1104 namespace_id: "namespace-registry-chio-example".to_string(),
1105 lifecycle_state: GenericNamespaceLifecycleState::Active,
1106 ownership: GenericNamespaceOwnership {
1107 namespace: "https://registry.chio.example".to_string(),
1108 owner_id: owner_id.to_string(),
1109 owner_name: Some("Registry Operator".to_string()),
1110 registry_url: "https://registry.chio.example".to_string(),
1111 signer_public_key: signing_keypair.public_key(),
1112 registered_at: 100,
1113 transferred_from_owner_id: None,
1114 },
1115 boundary: GenericListingBoundary::default(),
1116 };
1117 let listing = GenericListingArtifact {
1118 schema: GENERIC_LISTING_ARTIFACT_SCHEMA.to_string(),
1119 listing_id: "listing-demo".to_string(),
1120 namespace: namespace.ownership.namespace.clone(),
1121 published_at: 200,
1122 expires_at: Some(500),
1123 status: GenericListingStatus::Active,
1124 namespace_ownership: namespace.ownership.clone(),
1125 subject: GenericListingSubject {
1126 actor_kind: GenericListingActorKind::ToolServer,
1127 actor_id: "demo-server".to_string(),
1128 display_name: Some("Demo Server".to_string()),
1129 metadata_url: Some("https://registry.chio.example/servers/demo".to_string()),
1130 resolution_url: None,
1131 homepage_url: None,
1132 },
1133 compatibility: GenericListingCompatibilityReference {
1134 source_schema: "chio.certify.check.v1".to_string(),
1135 source_artifact_id: "cert-check-demo".to_string(),
1136 source_artifact_sha256: "sha256-demo".to_string(),
1137 },
1138 boundary: GenericListingBoundary::default(),
1139 };
1140 SignedGenericListing::sign(listing, signing_keypair).expect("sign listing")
1141 }
1142
1143 fn sample_publisher(owner_id: &str) -> GenericRegistryPublisher {
1144 GenericRegistryPublisher {
1145 role: GenericRegistryPublisherRole::Origin,
1146 operator_id: owner_id.to_string(),
1147 operator_name: Some("Registry Operator".to_string()),
1148 registry_url: "https://registry.chio.example".to_string(),
1149 upstream_registry_urls: Vec::new(),
1150 }
1151 }
1152
1153 fn sample_activation(
1154 owner_id: &str,
1155 signing_keypair: &Keypair,
1156 listing: &SignedGenericListing,
1157 ) -> SignedGenericTrustActivation {
1158 let artifact = build_generic_trust_activation_artifact(
1159 owner_id,
1160 Some("Registry Operator".to_string()),
1161 &GenericTrustActivationIssueRequest {
1162 listing: listing.clone(),
1163 admission_class: GenericTrustAdmissionClass::BondBacked,
1164 disposition: GenericTrustActivationDisposition::Approved,
1165 eligibility: GenericTrustActivationEligibility {
1166 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1167 allowed_publisher_roles: vec![GenericRegistryPublisherRole::Origin],
1168 allowed_statuses: vec![GenericListingStatus::Active],
1169 require_fresh_listing: true,
1170 require_bond_backing: true,
1171 required_listing_operator_ids: vec![owner_id.to_string()],
1172 policy_reference: Some("policy/open-market/default".to_string()),
1173 },
1174 review_context: GenericTrustActivationReviewContext {
1175 publisher: sample_publisher(owner_id),
1176 freshness: GenericListingReplicaFreshness {
1177 state: GenericListingFreshnessState::Fresh,
1178 age_secs: 0,
1179 max_age_secs: 300,
1180 valid_until: 500,
1181 generated_at: 200,
1182 },
1183 },
1184 requested_by: "ops@chio.example".to_string(),
1185 reviewed_by: Some("reviewer@chio.example".to_string()),
1186 requested_at: Some(200),
1187 reviewed_at: Some(201),
1188 expires_at: Some(450),
1189 note: None,
1190 },
1191 200,
1192 )
1193 .expect("build activation");
1194 SignedGenericTrustActivation::sign(artifact, signing_keypair).expect("sign activation")
1195 }
1196
1197 fn sample_charter(owner_id: &str, signing_keypair: &Keypair) -> SignedGenericGovernanceCharter {
1198 let artifact = build_generic_governance_charter_artifact(
1199 owner_id,
1200 Some("Registry Operator".to_string()),
1201 &GenericGovernanceCharterIssueRequest {
1202 authority_scope: GenericGovernanceAuthorityScope {
1203 namespace: "https://registry.chio.example".to_string(),
1204 allowed_listing_operator_ids: vec![owner_id.to_string()],
1205 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1206 policy_reference: Some("policy/governance/default".to_string()),
1207 },
1208 allowed_case_kinds: vec![
1209 GenericGovernanceCaseKind::Sanction,
1210 GenericGovernanceCaseKind::Appeal,
1211 ],
1212 escalation_operator_ids: Vec::new(),
1213 issued_by: "governance@chio.example".to_string(),
1214 issued_at: Some(202),
1215 expires_at: Some(600),
1216 note: None,
1217 },
1218 202,
1219 )
1220 .expect("build charter");
1221 SignedGenericGovernanceCharter::sign(artifact, signing_keypair).expect("sign charter")
1222 }
1223
1224 fn sample_sanction_case(
1225 owner_id: &str,
1226 signing_keypair: &Keypair,
1227 listing: &SignedGenericListing,
1228 activation: &SignedGenericTrustActivation,
1229 charter: &SignedGenericGovernanceCharter,
1230 ) -> SignedGenericGovernanceCase {
1231 let artifact = build_generic_governance_case_artifact(
1232 owner_id,
1233 &GenericGovernanceCaseIssueRequest {
1234 charter: charter.clone(),
1235 listing: listing.clone(),
1236 activation: Some(activation.clone()),
1237 kind: GenericGovernanceCaseKind::Sanction,
1238 state: GenericGovernanceCaseState::Enforced,
1239 subject_operator_id: Some(owner_id.to_string()),
1240 escalated_to_operator_ids: Vec::new(),
1241 evidence_refs: vec![GenericGovernanceEvidenceReference {
1242 kind: GenericGovernanceEvidenceKind::TrustActivation,
1243 reference_id: activation.body.activation_id.clone(),
1244 uri: None,
1245 sha256: None,
1246 }],
1247 appeal_of_case_id: None,
1248 supersedes_case_id: None,
1249 issued_by: "governance@chio.example".to_string(),
1250 opened_at: Some(203),
1251 updated_at: Some(203),
1252 expires_at: Some(500),
1253 note: None,
1254 },
1255 203,
1256 )
1257 .expect("build case");
1258 SignedGenericGovernanceCase::sign(artifact, signing_keypair).expect("sign case")
1259 }
1260
1261 fn sample_fee_schedule(
1262 owner_id: &str,
1263 signing_keypair: &Keypair,
1264 ) -> SignedOpenMarketFeeSchedule {
1265 let artifact = build_open_market_fee_schedule_artifact(
1266 owner_id,
1267 Some("Registry Operator".to_string()),
1268 &OpenMarketFeeScheduleIssueRequest {
1269 scope: OpenMarketEconomicsScope {
1270 namespace: "https://registry.chio.example".to_string(),
1271 allowed_listing_operator_ids: vec![owner_id.to_string()],
1272 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1273 allowed_admission_classes: vec![GenericTrustAdmissionClass::BondBacked],
1274 policy_reference: Some("policy/open-market/default".to_string()),
1275 },
1276 publication_fee: MonetaryAmount {
1277 units: 100,
1278 currency: "USD".to_string(),
1279 },
1280 dispute_fee: MonetaryAmount {
1281 units: 2500,
1282 currency: "USD".to_string(),
1283 },
1284 market_participation_fee: MonetaryAmount {
1285 units: 500,
1286 currency: "USD".to_string(),
1287 },
1288 bond_requirements: vec![OpenMarketBondRequirement {
1289 bond_class: OpenMarketBondClass::Listing,
1290 required_amount: MonetaryAmount {
1291 units: 5000,
1292 currency: "USD".to_string(),
1293 },
1294 collateral_reference_kind: OpenMarketCollateralReferenceKind::CreditBond,
1295 slashable: true,
1296 }],
1297 issued_by: "market@chio.example".to_string(),
1298 issued_at: Some(202),
1299 expires_at: Some(600),
1300 note: None,
1301 },
1302 202,
1303 )
1304 .expect("build fee schedule");
1305 SignedOpenMarketFeeSchedule::sign(artifact, signing_keypair).expect("sign fee schedule")
1306 }
1307
1308 fn sample_penalty_issue_request(
1309 owner_id: &str,
1310 fee_schedule: SignedOpenMarketFeeSchedule,
1311 charter: SignedGenericGovernanceCharter,
1312 case: SignedGenericGovernanceCase,
1313 listing: SignedGenericListing,
1314 activation: Option<SignedGenericTrustActivation>,
1315 ) -> OpenMarketPenaltyIssueRequest {
1316 OpenMarketPenaltyIssueRequest {
1317 fee_schedule,
1318 charter,
1319 case,
1320 listing,
1321 activation,
1322 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1323 bond_class: OpenMarketBondClass::Listing,
1324 action: OpenMarketPenaltyAction::SlashBond,
1325 state: OpenMarketPenaltyState::Enforced,
1326 penalty_amount: MonetaryAmount {
1327 units: 2500,
1328 currency: "USD".to_string(),
1329 },
1330 evidence_refs: vec![OpenMarketEvidenceReference {
1331 kind: OpenMarketEvidenceKind::GovernanceCase,
1332 reference_id: "case-ref".to_string(),
1333 uri: None,
1334 sha256: None,
1335 }],
1336 subject_operator_id: Some(owner_id.to_string()),
1337 supersedes_penalty_id: None,
1338 issued_by: "market@chio.example".to_string(),
1339 opened_at: Some(204),
1340 updated_at: Some(204),
1341 expires_at: Some(500),
1342 note: None,
1343 }
1344 }
1345
1346 #[test]
1347 fn open_market_evaluation_applies_fee_schedule_and_slash_penalty() {
1348 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1349 let owner_id = "https://registry.chio.example";
1350 let listing = sample_listing(owner_id, &signing_keypair);
1351 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1352 let charter = sample_charter(owner_id, &signing_keypair);
1353 let governance_case =
1354 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1355 let fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1356 let penalty_artifact = build_open_market_penalty_artifact(
1357 owner_id,
1358 &OpenMarketPenaltyIssueRequest {
1359 fee_schedule: fee_schedule.clone(),
1360 charter: charter.clone(),
1361 case: governance_case.clone(),
1362 listing: listing.clone(),
1363 activation: Some(activation.clone()),
1364 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1365 bond_class: OpenMarketBondClass::Listing,
1366 action: OpenMarketPenaltyAction::SlashBond,
1367 state: OpenMarketPenaltyState::Enforced,
1368 penalty_amount: MonetaryAmount {
1369 units: 2500,
1370 currency: "USD".to_string(),
1371 },
1372 evidence_refs: vec![OpenMarketEvidenceReference {
1373 kind: OpenMarketEvidenceKind::GovernanceCase,
1374 reference_id: governance_case.body.case_id.clone(),
1375 uri: None,
1376 sha256: None,
1377 }],
1378 subject_operator_id: Some(owner_id.to_string()),
1379 supersedes_penalty_id: None,
1380 issued_by: "market@chio.example".to_string(),
1381 opened_at: Some(204),
1382 updated_at: Some(204),
1383 expires_at: Some(500),
1384 note: None,
1385 },
1386 204,
1387 )
1388 .expect("build penalty");
1389 let penalty = SignedOpenMarketPenalty::sign(penalty_artifact, &signing_keypair)
1390 .expect("sign penalty");
1391
1392 let evaluation = evaluate_open_market_penalty(
1393 &OpenMarketPenaltyEvaluationRequest {
1394 fee_schedule,
1395 listing,
1396 current_publisher: sample_publisher(owner_id),
1397 activation: Some(activation),
1398 charter,
1399 case: governance_case,
1400 penalty,
1401 prior_penalty: None,
1402 evaluated_at: Some(205),
1403 },
1404 205,
1405 )
1406 .expect("evaluate open market");
1407
1408 assert_eq!(
1409 evaluation.effective_state,
1410 OpenMarketPenaltyEffectiveState::BondSlashed
1411 );
1412 assert!(evaluation.blocks_admission);
1413 assert!(evaluation.findings.is_empty());
1414 assert_eq!(
1415 evaluation
1416 .publication_fee
1417 .as_ref()
1418 .expect("publication fee")
1419 .units,
1420 100
1421 );
1422 assert_eq!(
1423 evaluation
1424 .bond_requirement
1425 .as_ref()
1426 .expect("bond requirement")
1427 .bond_class,
1428 OpenMarketBondClass::Listing
1429 );
1430 }
1431
1432 #[test]
1433 fn open_market_evaluation_rejects_expired_fee_schedule() {
1434 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1435 let owner_id = "https://registry.chio.example";
1436 let listing = sample_listing(owner_id, &signing_keypair);
1437 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1438 let charter = sample_charter(owner_id, &signing_keypair);
1439 let governance_case =
1440 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1441 let mut fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1442 fee_schedule.body.expires_at = Some(204);
1443 let fee_schedule =
1444 SignedOpenMarketFeeSchedule::sign(fee_schedule.body, &signing_keypair).expect("resign");
1445 let penalty_artifact = build_open_market_penalty_artifact(
1446 owner_id,
1447 &OpenMarketPenaltyIssueRequest {
1448 fee_schedule: fee_schedule.clone(),
1449 charter: charter.clone(),
1450 case: governance_case.clone(),
1451 listing: listing.clone(),
1452 activation: Some(activation.clone()),
1453 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1454 bond_class: OpenMarketBondClass::Listing,
1455 action: OpenMarketPenaltyAction::HoldBond,
1456 state: OpenMarketPenaltyState::Enforced,
1457 penalty_amount: MonetaryAmount {
1458 units: 1000,
1459 currency: "USD".to_string(),
1460 },
1461 evidence_refs: vec![OpenMarketEvidenceReference {
1462 kind: OpenMarketEvidenceKind::GovernanceCase,
1463 reference_id: governance_case.body.case_id.clone(),
1464 uri: None,
1465 sha256: None,
1466 }],
1467 subject_operator_id: Some(owner_id.to_string()),
1468 supersedes_penalty_id: None,
1469 issued_by: "market@chio.example".to_string(),
1470 opened_at: Some(204),
1471 updated_at: Some(204),
1472 expires_at: Some(500),
1473 note: None,
1474 },
1475 204,
1476 )
1477 .expect("build penalty");
1478 let penalty = SignedOpenMarketPenalty::sign(penalty_artifact, &signing_keypair)
1479 .expect("sign penalty");
1480
1481 let evaluation = evaluate_open_market_penalty(
1482 &OpenMarketPenaltyEvaluationRequest {
1483 fee_schedule,
1484 listing,
1485 current_publisher: sample_publisher(owner_id),
1486 activation: Some(activation),
1487 charter,
1488 case: governance_case,
1489 penalty,
1490 prior_penalty: None,
1491 evaluated_at: Some(205),
1492 },
1493 205,
1494 )
1495 .expect("evaluate open market");
1496
1497 assert_eq!(evaluation.findings.len(), 1);
1498 assert_eq!(
1499 evaluation.findings[0].code,
1500 OpenMarketFindingCode::FeeScheduleExpired
1501 );
1502 }
1503
1504 #[test]
1505 fn open_market_evaluation_rejects_missing_bond_requirement() {
1506 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1507 let owner_id = "https://registry.chio.example";
1508 let listing = sample_listing(owner_id, &signing_keypair);
1509 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1510 let charter = sample_charter(owner_id, &signing_keypair);
1511 let governance_case =
1512 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1513 let artifact = build_open_market_fee_schedule_artifact(
1514 owner_id,
1515 Some("Registry Operator".to_string()),
1516 &OpenMarketFeeScheduleIssueRequest {
1517 scope: OpenMarketEconomicsScope {
1518 namespace: "https://registry.chio.example".to_string(),
1519 allowed_listing_operator_ids: vec![owner_id.to_string()],
1520 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1521 allowed_admission_classes: vec![GenericTrustAdmissionClass::BondBacked],
1522 policy_reference: Some("policy/open-market/default".to_string()),
1523 },
1524 publication_fee: MonetaryAmount {
1525 units: 100,
1526 currency: "USD".to_string(),
1527 },
1528 dispute_fee: MonetaryAmount {
1529 units: 2500,
1530 currency: "USD".to_string(),
1531 },
1532 market_participation_fee: MonetaryAmount {
1533 units: 500,
1534 currency: "USD".to_string(),
1535 },
1536 bond_requirements: vec![OpenMarketBondRequirement {
1537 bond_class: OpenMarketBondClass::Dispute,
1538 required_amount: MonetaryAmount {
1539 units: 5000,
1540 currency: "USD".to_string(),
1541 },
1542 collateral_reference_kind: OpenMarketCollateralReferenceKind::CreditBond,
1543 slashable: true,
1544 }],
1545 issued_by: "market@chio.example".to_string(),
1546 issued_at: Some(202),
1547 expires_at: Some(600),
1548 note: None,
1549 },
1550 202,
1551 )
1552 .expect("build fee schedule");
1553 let fee_schedule = SignedOpenMarketFeeSchedule::sign(artifact, &signing_keypair)
1554 .expect("sign fee schedule");
1555 let penalty_artifact = build_open_market_penalty_artifact(
1556 owner_id,
1557 &OpenMarketPenaltyIssueRequest {
1558 fee_schedule: fee_schedule.clone(),
1559 charter: charter.clone(),
1560 case: governance_case.clone(),
1561 listing: listing.clone(),
1562 activation: Some(activation.clone()),
1563 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1564 bond_class: OpenMarketBondClass::Listing,
1565 action: OpenMarketPenaltyAction::HoldBond,
1566 state: OpenMarketPenaltyState::Enforced,
1567 penalty_amount: MonetaryAmount {
1568 units: 1000,
1569 currency: "USD".to_string(),
1570 },
1571 evidence_refs: vec![OpenMarketEvidenceReference {
1572 kind: OpenMarketEvidenceKind::GovernanceCase,
1573 reference_id: governance_case.body.case_id.clone(),
1574 uri: None,
1575 sha256: None,
1576 }],
1577 subject_operator_id: Some(owner_id.to_string()),
1578 supersedes_penalty_id: None,
1579 issued_by: "market@chio.example".to_string(),
1580 opened_at: Some(204),
1581 updated_at: Some(204),
1582 expires_at: Some(500),
1583 note: None,
1584 },
1585 204,
1586 )
1587 .expect("build penalty");
1588 let penalty = SignedOpenMarketPenalty::sign(penalty_artifact, &signing_keypair)
1589 .expect("sign penalty");
1590
1591 let evaluation = evaluate_open_market_penalty(
1592 &OpenMarketPenaltyEvaluationRequest {
1593 fee_schedule,
1594 listing,
1595 current_publisher: sample_publisher(owner_id),
1596 activation: Some(activation),
1597 charter,
1598 case: governance_case,
1599 penalty,
1600 prior_penalty: None,
1601 evaluated_at: Some(205),
1602 },
1603 205,
1604 )
1605 .expect("evaluate open market");
1606
1607 assert_eq!(evaluation.findings.len(), 1);
1608 assert_eq!(
1609 evaluation.findings[0].code,
1610 OpenMarketFindingCode::BondRequirementMissing
1611 );
1612 }
1613
1614 #[test]
1615 fn open_market_penalty_issue_rejects_non_local_activation_authority() {
1616 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1617 let owner_id = "https://registry.chio.example";
1618 let listing = sample_listing(owner_id, &signing_keypair);
1619 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1620 let mut forged_activation_body = activation.body.clone();
1621 forged_activation_body.local_operator_id = "https://remote.chio.example".to_string();
1622 forged_activation_body.local_operator_name = Some("Remote Operator".to_string());
1623 let forged_activation =
1624 SignedGenericTrustActivation::sign(forged_activation_body, &Keypair::generate())
1625 .expect("sign forged activation");
1626 let charter = sample_charter(owner_id, &signing_keypair);
1627 let governance_case =
1628 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1629 let fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1630
1631 let error = build_open_market_penalty_artifact(
1632 owner_id,
1633 &OpenMarketPenaltyIssueRequest {
1634 fee_schedule,
1635 charter,
1636 case: governance_case.clone(),
1637 listing,
1638 activation: Some(forged_activation),
1639 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1640 bond_class: OpenMarketBondClass::Listing,
1641 action: OpenMarketPenaltyAction::SlashBond,
1642 state: OpenMarketPenaltyState::Enforced,
1643 penalty_amount: MonetaryAmount {
1644 units: 2500,
1645 currency: "USD".to_string(),
1646 },
1647 evidence_refs: vec![OpenMarketEvidenceReference {
1648 kind: OpenMarketEvidenceKind::GovernanceCase,
1649 reference_id: governance_case.body.case_id,
1650 uri: None,
1651 sha256: None,
1652 }],
1653 subject_operator_id: Some(owner_id.to_string()),
1654 supersedes_penalty_id: None,
1655 issued_by: "market@chio.example".to_string(),
1656 opened_at: Some(204),
1657 updated_at: Some(204),
1658 expires_at: Some(500),
1659 note: None,
1660 },
1661 204,
1662 )
1663 .expect_err("non-local activation authority rejected");
1664 assert!(error.contains("issued by the governing operator"));
1665 }
1666
1667 #[test]
1668 fn open_market_evaluation_rejects_non_local_activation_authority() {
1669 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1670 let owner_id = "https://registry.chio.example";
1671 let listing = sample_listing(owner_id, &signing_keypair);
1672 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1673 let charter = sample_charter(owner_id, &signing_keypair);
1674 let governance_case =
1675 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1676 let fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1677 let penalty_artifact = build_open_market_penalty_artifact(
1678 owner_id,
1679 &OpenMarketPenaltyIssueRequest {
1680 fee_schedule: fee_schedule.clone(),
1681 charter: charter.clone(),
1682 case: governance_case.clone(),
1683 listing: listing.clone(),
1684 activation: Some(activation.clone()),
1685 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1686 bond_class: OpenMarketBondClass::Listing,
1687 action: OpenMarketPenaltyAction::SlashBond,
1688 state: OpenMarketPenaltyState::Enforced,
1689 penalty_amount: MonetaryAmount {
1690 units: 2500,
1691 currency: "USD".to_string(),
1692 },
1693 evidence_refs: vec![OpenMarketEvidenceReference {
1694 kind: OpenMarketEvidenceKind::GovernanceCase,
1695 reference_id: governance_case.body.case_id.clone(),
1696 uri: None,
1697 sha256: None,
1698 }],
1699 subject_operator_id: Some(owner_id.to_string()),
1700 supersedes_penalty_id: None,
1701 issued_by: "market@chio.example".to_string(),
1702 opened_at: Some(204),
1703 updated_at: Some(204),
1704 expires_at: Some(500),
1705 note: None,
1706 },
1707 204,
1708 )
1709 .expect("build penalty");
1710 let penalty = SignedOpenMarketPenalty::sign(penalty_artifact, &signing_keypair)
1711 .expect("sign penalty");
1712 let mut forged_activation_body = activation.body.clone();
1713 forged_activation_body.local_operator_id = "https://remote.chio.example".to_string();
1714 forged_activation_body.local_operator_name = Some("Remote Operator".to_string());
1715 let forged_activation =
1716 SignedGenericTrustActivation::sign(forged_activation_body, &Keypair::generate())
1717 .expect("sign forged activation");
1718
1719 let evaluation = evaluate_open_market_penalty(
1720 &OpenMarketPenaltyEvaluationRequest {
1721 fee_schedule,
1722 listing,
1723 current_publisher: sample_publisher(owner_id),
1724 activation: Some(forged_activation),
1725 charter,
1726 case: governance_case,
1727 penalty,
1728 prior_penalty: None,
1729 evaluated_at: Some(205),
1730 },
1731 205,
1732 )
1733 .expect("evaluate open market");
1734
1735 assert_eq!(evaluation.findings.len(), 1);
1736 assert_eq!(
1737 evaluation.findings[0].code,
1738 OpenMarketFindingCode::ActivationMismatch
1739 );
1740 }
1741
1742 #[test]
1743 fn open_market_scope_rejects_blank_operator_ids() {
1744 let error = OpenMarketEconomicsScope {
1745 namespace: "https://registry.chio.example".to_string(),
1746 allowed_listing_operator_ids: vec![" ".to_string()],
1747 allowed_actor_kinds: Vec::new(),
1748 allowed_admission_classes: Vec::new(),
1749 policy_reference: None,
1750 }
1751 .validate()
1752 .expect_err("blank operator ids rejected");
1753
1754 assert!(error.contains("scope.allowed_listing_operator_ids[0]"));
1755 }
1756
1757 #[test]
1758 fn open_market_fee_schedule_validate_rejects_namespace_mismatch() {
1759 let error = OpenMarketFeeScheduleArtifact {
1760 schema: OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA.to_string(),
1761 fee_schedule_id: "fee-1".to_string(),
1762 namespace: "https://registry.chio.example".to_string(),
1763 governing_operator_id: "https://registry.chio.example".to_string(),
1764 governing_operator_name: Some("Registry Operator".to_string()),
1765 scope: OpenMarketEconomicsScope {
1766 namespace: "https://different.chio.example".to_string(),
1767 allowed_listing_operator_ids: vec!["https://registry.chio.example".to_string()],
1768 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1769 allowed_admission_classes: vec![GenericTrustAdmissionClass::BondBacked],
1770 policy_reference: None,
1771 },
1772 publication_fee: MonetaryAmount {
1773 units: 100,
1774 currency: "USD".to_string(),
1775 },
1776 dispute_fee: MonetaryAmount {
1777 units: 2500,
1778 currency: "USD".to_string(),
1779 },
1780 market_participation_fee: MonetaryAmount {
1781 units: 500,
1782 currency: "USD".to_string(),
1783 },
1784 bond_requirements: vec![OpenMarketBondRequirement {
1785 bond_class: OpenMarketBondClass::Listing,
1786 required_amount: MonetaryAmount {
1787 units: 5000,
1788 currency: "USD".to_string(),
1789 },
1790 collateral_reference_kind: OpenMarketCollateralReferenceKind::CreditBond,
1791 slashable: true,
1792 }],
1793 issued_at: 100,
1794 expires_at: Some(200),
1795 issued_by: "market@chio.example".to_string(),
1796 note: None,
1797 }
1798 .validate()
1799 .expect_err("namespace mismatch rejected");
1800
1801 assert!(error.contains("namespace must match scope namespace"));
1802 }
1803
1804 #[test]
1805 fn open_market_fee_schedule_issue_request_requires_bond_requirements() {
1806 let error = OpenMarketFeeScheduleIssueRequest {
1807 scope: OpenMarketEconomicsScope {
1808 namespace: "https://registry.chio.example".to_string(),
1809 allowed_listing_operator_ids: vec!["https://registry.chio.example".to_string()],
1810 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1811 allowed_admission_classes: vec![GenericTrustAdmissionClass::BondBacked],
1812 policy_reference: None,
1813 },
1814 publication_fee: MonetaryAmount {
1815 units: 100,
1816 currency: "USD".to_string(),
1817 },
1818 dispute_fee: MonetaryAmount {
1819 units: 2500,
1820 currency: "USD".to_string(),
1821 },
1822 market_participation_fee: MonetaryAmount {
1823 units: 500,
1824 currency: "USD".to_string(),
1825 },
1826 bond_requirements: Vec::new(),
1827 issued_by: "market@chio.example".to_string(),
1828 issued_at: Some(202),
1829 expires_at: Some(600),
1830 note: None,
1831 }
1832 .validate()
1833 .expect_err("bond requirements required");
1834
1835 assert!(error.contains("bond_requirements must not be empty"));
1836 }
1837
1838 #[test]
1839 fn open_market_penalty_validate_requires_reverse_slash_metadata() {
1840 let error = OpenMarketPenaltyArtifact {
1841 schema: OPEN_MARKET_PENALTY_ARTIFACT_SCHEMA.to_string(),
1842 penalty_id: "penalty-1".to_string(),
1843 fee_schedule_id: "fee-1".to_string(),
1844 charter_id: "charter-1".to_string(),
1845 case_id: "case-1".to_string(),
1846 governing_operator_id: "https://registry.chio.example".to_string(),
1847 namespace: "https://registry.chio.example".to_string(),
1848 listing_id: "listing-demo".to_string(),
1849 activation_id: Some("activation-1".to_string()),
1850 subject_operator_id: Some("https://registry.chio.example".to_string()),
1851 abuse_class: OpenMarketAbuseClass::UnverifiableListingBehavior,
1852 bond_class: OpenMarketBondClass::Listing,
1853 action: OpenMarketPenaltyAction::ReverseSlash,
1854 state: OpenMarketPenaltyState::Enforced,
1855 penalty_amount: MonetaryAmount {
1856 units: 2500,
1857 currency: "USD".to_string(),
1858 },
1859 opened_at: 100,
1860 updated_at: 100,
1861 expires_at: Some(200),
1862 evidence_refs: vec![OpenMarketEvidenceReference {
1863 kind: OpenMarketEvidenceKind::GovernanceCase,
1864 reference_id: "case-1".to_string(),
1865 uri: None,
1866 sha256: None,
1867 }],
1868 supersedes_penalty_id: None,
1869 issued_by: "market@chio.example".to_string(),
1870 note: None,
1871 }
1872 .validate()
1873 .expect_err("reverse slash metadata required");
1874
1875 assert!(error.contains("requires supersedes_penalty_id"));
1876 }
1877
1878 #[test]
1879 fn open_market_penalty_issue_request_rejects_invalid_fee_schedule_signature() {
1880 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1881 let owner_id = "https://registry.chio.example";
1882 let listing = sample_listing(owner_id, &signing_keypair);
1883 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1884 let charter = sample_charter(owner_id, &signing_keypair);
1885 let governance_case =
1886 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1887 let fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1888 let mut tampered_fee_schedule = fee_schedule.clone();
1889 tampered_fee_schedule.body.publication_fee.units += 1;
1890
1891 let error = sample_penalty_issue_request(
1892 owner_id,
1893 tampered_fee_schedule,
1894 charter,
1895 governance_case,
1896 listing,
1897 Some(activation),
1898 )
1899 .validate()
1900 .expect_err("tampered fee schedule rejected");
1901
1902 assert!(error.contains("fee schedule signature is invalid"));
1903 }
1904
1905 #[test]
1906 fn build_open_market_fee_schedule_artifact_uses_request_issued_at() {
1907 let owner_id = "https://registry.chio.example";
1908 let mut request = OpenMarketFeeScheduleIssueRequest {
1909 scope: OpenMarketEconomicsScope {
1910 namespace: "https://registry.chio.example".to_string(),
1911 allowed_listing_operator_ids: vec![owner_id.to_string()],
1912 allowed_actor_kinds: vec![GenericListingActorKind::ToolServer],
1913 allowed_admission_classes: vec![GenericTrustAdmissionClass::BondBacked],
1914 policy_reference: Some("policy/open-market/default".to_string()),
1915 },
1916 publication_fee: MonetaryAmount {
1917 units: 100,
1918 currency: "USD".to_string(),
1919 },
1920 dispute_fee: MonetaryAmount {
1921 units: 2500,
1922 currency: "USD".to_string(),
1923 },
1924 market_participation_fee: MonetaryAmount {
1925 units: 500,
1926 currency: "USD".to_string(),
1927 },
1928 bond_requirements: vec![OpenMarketBondRequirement {
1929 bond_class: OpenMarketBondClass::Listing,
1930 required_amount: MonetaryAmount {
1931 units: 5000,
1932 currency: "USD".to_string(),
1933 },
1934 collateral_reference_kind: OpenMarketCollateralReferenceKind::CreditBond,
1935 slashable: true,
1936 }],
1937 issued_by: "market@chio.example".to_string(),
1938 issued_at: Some(777),
1939 expires_at: Some(900),
1940 note: None,
1941 };
1942 let artifact = build_open_market_fee_schedule_artifact(
1943 owner_id,
1944 Some("Registry Operator".to_string()),
1945 &request,
1946 202,
1947 )
1948 .expect("build fee schedule");
1949
1950 assert_eq!(artifact.issued_at, 777);
1951 assert_eq!(artifact.governing_operator_id, owner_id);
1952 assert!(artifact.fee_schedule_id.starts_with("market-fee-"));
1953 request.issued_at = Some(778);
1954 let changed = build_open_market_fee_schedule_artifact(
1955 owner_id,
1956 Some("Registry Operator".to_string()),
1957 &request,
1958 202,
1959 )
1960 .expect("build changed fee schedule");
1961 assert_ne!(artifact.fee_schedule_id, changed.fee_schedule_id);
1962 }
1963
1964 #[test]
1965 fn open_market_evaluation_rejects_invalid_penalty_signature() {
1966 let signing_keypair = Keypair::from_seed(&[7_u8; 32]);
1967 let owner_id = "https://registry.chio.example";
1968 let listing = sample_listing(owner_id, &signing_keypair);
1969 let activation = sample_activation(owner_id, &signing_keypair, &listing);
1970 let charter = sample_charter(owner_id, &signing_keypair);
1971 let governance_case =
1972 sample_sanction_case(owner_id, &signing_keypair, &listing, &activation, &charter);
1973 let fee_schedule = sample_fee_schedule(owner_id, &signing_keypair);
1974 let penalty_artifact = build_open_market_penalty_artifact(
1975 owner_id,
1976 &sample_penalty_issue_request(
1977 owner_id,
1978 fee_schedule.clone(),
1979 charter.clone(),
1980 governance_case.clone(),
1981 listing.clone(),
1982 Some(activation.clone()),
1983 ),
1984 204,
1985 )
1986 .expect("build penalty");
1987 let penalty = SignedOpenMarketPenalty::sign(penalty_artifact, &signing_keypair)
1988 .expect("sign penalty");
1989 let mut tampered_penalty = penalty.clone();
1990 tampered_penalty.body.note = Some("tampered".to_string());
1991
1992 let evaluation = evaluate_open_market_penalty(
1993 &OpenMarketPenaltyEvaluationRequest {
1994 fee_schedule,
1995 listing,
1996 current_publisher: sample_publisher(owner_id),
1997 activation: Some(activation),
1998 charter,
1999 case: governance_case,
2000 penalty: tampered_penalty,
2001 prior_penalty: None,
2002 evaluated_at: Some(205),
2003 },
2004 205,
2005 )
2006 .expect("evaluate open market");
2007
2008 assert_eq!(
2009 evaluation.findings[0].code,
2010 OpenMarketFindingCode::PenaltyUnverifiable
2011 );
2012 }
2013}