Skip to main content

chio_open_market/
lib.rs

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}