Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/catalyst-contest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ catalyst-voting = { version = "0.0.2", git = "https://github.com/input-output-hk

cbork-utils = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils/v0.0.4" }

futures = "0.3.31"
minicbor = { version = "0.25.1", features = ["std"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
Expand All @@ -31,3 +30,4 @@ strum = { version = "0.27", features = ["derive"] }
[dev-dependencies]
proptest = { version = "1.6.0", features = ["attr-macro"] }
proptest-derive = "0.5.1"
test-case = "3.3.1"
155 changes: 129 additions & 26 deletions rust/catalyst-contest/src/contest_delegation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
//!
//! [documentation]: https://docs.dev.projectcatalyst.io/libs/main/architecture/08_concepts/signed_doc/docs/contest_delegation/#contest-delegation

pub mod rule;

#[cfg(test)]
mod tests;

use anyhow::Context;
use catalyst_signed_doc::{
CatalystSignedDocument, DocumentRef,
catalyst_id::CatalystId,
doc_types::CONTEST_DELEGATION,
doc_types::{self, CONTEST_DELEGATION},
problem_report::ProblemReport,
providers::{CatalystSignedDocumentProvider, Provider},
providers::{
CatalystIdSelector, CatalystSignedDocumentProvider, CatalystSignedDocumentSearchQuery,
DocTypeSelector, DocumentRefSelector,
},
uuid::UuidV7,
validator::CatalystSignedDocumentValidationRule,
};

/// `Contest Delegation` document type.
Expand Down Expand Up @@ -94,28 +102,6 @@ impl ContestDelegation {
}
}

/// `CatalystSignedDocumentValidationRule` implementation for Contest Delegation document.
#[derive(Debug)]
pub struct ContestDelegationRule;

impl CatalystSignedDocumentValidationRule for ContestDelegationRule {
fn check(
&self,
doc: &CatalystSignedDocument,
provider: &dyn Provider,
) -> anyhow::Result<bool> {
let mut valid = true;

valid &= get_delegator(doc, doc.report()).1;
let (payload, is_payload_valid) = get_payload(doc, doc.report());
valid &= is_payload_valid;

valid &= get_delegations(doc, payload, provider, doc.report())?.1;

Ok(valid)
}
}

impl ContestDelegation {
/// Trying to build Contest Delegation document collecting all issues into the
/// `report`.
Expand Down Expand Up @@ -206,6 +192,46 @@ fn get_payload(
(payload, valid)
}

/// Get the 'Contest Parameters' document from the 'parameters' metadata field, applying
/// all necessary validations.
/// Returns boolean flag, was it valid or not.
fn contest_parameters_checks(
doc: &CatalystSignedDocument,
provider: &dyn CatalystSignedDocumentProvider,
report: &ProblemReport,
) -> anyhow::Result<bool> {
let Some(doc_ref) = doc.doc_meta().parameters().and_then(|v| v.first()) else {
report.missing_field(
"parameters",
"Contest Delegation must have a 'parameters' metadata field",
);
report.missing_field(
"parameters",
"Contest Delegation must have a 'parameters' metadata field",
);
return Ok(false);
};

let Some(_contest_parameters) = provider.try_get_doc(doc_ref)? else {
report.functional_validation(
&format!("Cannot get referenced document by reference: {doc_ref}"),
"Missing 'Contest Parameters' document for the Contest Delegation document",
);
return Ok(false);
};

let Ok(_doc_ver) = doc.doc_ver() else {
report.missing_field(
"ver",
"Missing 'ver' metadata field for 'Contest Delegation' document",
);
return Ok(false);
};

// TODO: apply time based checks
Ok(true)
}

/// Get a list of delegations
/// Returns additional boolean flag, was it valid or not.
fn get_delegations(
Expand Down Expand Up @@ -265,12 +291,14 @@ fn get_author_kid(
let Some(ref_doc) = provider.try_get_doc(doc_ref)? else {
report.functional_validation(
&format!("Cannot get referenced document by reference: {doc_ref}"),
"Missing representative reference document for the Contest Delegation document",
"Missing 'Rep Nomination' document for the Contest Delegation document",
);
valid = false;
return Ok((None, valid));
};

valid &= rep_nomination_ref_check(&ref_doc, provider, report)?;

let rep_nomination_authors = ref_doc.authors();
if rep_nomination_authors.len() != 1 {
report.invalid_value(
Expand All @@ -285,3 +313,78 @@ fn get_author_kid(
let rep_kid = rep_nomination_authors.into_iter().next();
Ok((rep_kid, valid))
}

/// Verifies that the corresponding 'Rep Nomination' document reference is valid:
/// - References to the latest version of 'Rep Nomination' document ever submitted to the
/// corresponding 'Contest Parameters' document.
/// - A Representative MUST Delegate to their latest Nomination for a 'Contest
/// Parameters', otherwise their Nomination is invalid.
fn rep_nomination_ref_check(
ref_doc: &CatalystSignedDocument,
provider: &dyn CatalystSignedDocumentProvider,
report: &ProblemReport,
) -> anyhow::Result<bool> {
let mut valid = true;

// We could use 'Rep Nomination'->'parameters' field,
// because it must be the same as the 'Contest Delegation' document according the
// `ParametersRule::link_check` verification.
let Some(parameters) = ref_doc.doc_meta().parameters() else {
report.missing_field(
"parameters",
"Missing 'parameters' metadata field for the 'Rep Nomination' document during 'Content Delegation' validation"
);
return Ok(false);
};
// Trying to find ALL available 'Rep Nomination' documents which reference to the `Contest
// Parameters`
let query = CatalystSignedDocumentSearchQuery {
authors: Some(CatalystIdSelector::Eq(ref_doc.authors())),
parameters: Some(DocumentRefSelector::Eq(parameters.clone())),
doc_type: Some(DocTypeSelector::In(vec![doc_types::REP_NOMINATION])),
..Default::default()
};
let all_nominations = provider.try_search_docs(&query)?;

let Ok(ref_doc_ref) = ref_doc.doc_ref() else {
report.missing_field(
"document reference",
"Cannot get document reference for the 'Rep Nomination' document during 'Content Delegation' validation",
);
return Ok(false);
};

let latest_ref_doc_ref = all_nominations
.iter()
.filter_map(|doc| doc.doc_ref().ok())
// TODO: replace it with just `max` after https://github.com/input-output-hk/catalyst-libs/issues/751 would be resolved
.max_by_key(|v| v.ver().uuid())
.context("A latest version of the document must exist if a first version exists")?;

if latest_ref_doc_ref != ref_doc_ref {
report.functional_validation(
"It must be the latest Rep Nomination document",
"Content Delegation must reference to the latest version Rep Nomination document",
);
valid = false;
}

// Trying to find the latest 'Contest Delegation' submitted by the representative ('Rep
// Nomination' author/signer).
let query = CatalystSignedDocumentSearchQuery {
authors: Some(CatalystIdSelector::Eq(ref_doc.authors())),
parameters: Some(DocumentRefSelector::Eq(parameters.clone())),
doc_ref: Some(DocumentRefSelector::Eq(vec![ref_doc_ref].into())),
doc_type: Some(DocTypeSelector::In(vec![doc_types::CONTEST_DELEGATION])),
..Default::default()
};
if provider.try_search_docs(&query)?.is_empty() {
report.functional_validation(
"A Representative MUST Delegate to their latest Nomination for a 'Contest Parameters', otherwise their Nomination is invalid.",
"Fails to validate a 'Contest Delegation' referenced representative nomination"
);
valid = false;
}

Ok(valid)
}
31 changes: 31 additions & 0 deletions rust/catalyst-contest/src/contest_delegation/rule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Implementation of the
//! `catalyst_signed_doc::validator::CatalystSignedDocumentValidationRule` trait for the
//! `Contest Delegation` document

use catalyst_signed_doc::{
CatalystSignedDocument, providers::Provider, validator::CatalystSignedDocumentValidationRule,
};

use super::{contest_parameters_checks, get_delegations, get_delegator, get_payload};

/// `CatalystSignedDocumentValidationRule` implementation for Contest Delegation document.
#[derive(Debug)]
pub struct ContestDelegationRule;

impl CatalystSignedDocumentValidationRule for ContestDelegationRule {
fn check(
&self,
doc: &CatalystSignedDocument,
provider: &dyn Provider,
) -> anyhow::Result<bool> {
let mut valid = true;

valid &= get_delegator(doc, doc.report()).1;
let (payload, is_payload_valid) = get_payload(doc, doc.report());
valid &= is_payload_valid;
valid &= contest_parameters_checks(doc, provider, doc.report())?;
valid &= get_delegations(doc, payload, provider, doc.report())?.1;

Ok(valid)
}
}
95 changes: 95 additions & 0 deletions rust/catalyst-contest/src/contest_delegation/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Test for 'Contest Delegation' document validation part.
//! <https://docs.dev.projectcatalyst.io/libs/main/architecture/08_concepts/signed_doc/docs/contest_delegation>

use catalyst_signed_doc::{
providers::tests::TestCatalystProvider,
tests_utils::{
brand_parameters_doc, brand_parameters_form_template_doc,
contest_delegation_by_representative_doc, contest_delegation_doc, contest_parameters_doc,
contest_parameters_form_template_doc, rep_nomination_doc, rep_nomination_form_template_doc,
rep_profile_doc, rep_profile_form_template_doc,
},
validator::Validator,
*,
};
use test_case::test_case;

use crate::contest_delegation::{ContestDelegation, rule::ContestDelegationRule};

#[test_case(
|provider| {
let template = brand_parameters_form_template_doc(provider).inspect(|v| provider.add_document(v).unwrap())?;
let brand = brand_parameters_doc(&template, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_profile_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_profile = rep_profile_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = contest_parameters_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let contest = contest_parameters_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_nomination_form_template_doc(&contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_nomination = rep_nomination_doc(&template, &rep_profile, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let _delegation_by_representative = contest_delegation_by_representative_doc(&rep_nomination, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
contest_delegation_doc(&rep_nomination, &contest, provider)
}
=> true
;
"valid document"
)]
#[test_case(
|provider| {
let template = brand_parameters_form_template_doc(provider).inspect(|v| provider.add_document(v).unwrap())?;
let brand = brand_parameters_doc(&template, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_profile_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_profile = rep_profile_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = contest_parameters_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let contest = contest_parameters_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_nomination_form_template_doc(&contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_nomination = rep_nomination_doc(&template, &rep_profile, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
contest_delegation_doc(&rep_nomination, &contest, provider)
}
=> false
;
"missing delegation by representative"
)]
#[test_case(
|provider| {
let template = brand_parameters_form_template_doc(provider).inspect(|v| provider.add_document(v).unwrap())?;
let brand = brand_parameters_doc(&template, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_profile_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_profile = rep_profile_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = contest_parameters_form_template_doc(&brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let contest = contest_parameters_doc(&template, &brand, provider).inspect(|v| provider.add_document(v).unwrap())?;
let template = rep_nomination_form_template_doc(&contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let rep_nomination = rep_nomination_doc(&template, &rep_profile, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let _delegation_by_representative = contest_delegation_by_representative_doc(&rep_nomination, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
std::thread::sleep(std::time::Duration::from_secs(1));
let rep_nomination_latest = rep_nomination_doc(&template, &rep_profile, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
let _delegation_by_representative_2 = contest_delegation_by_representative_doc(&rep_nomination_latest, &contest, provider).inspect(|v| provider.add_document(v).unwrap())?;
contest_delegation_doc(&rep_nomination, &contest, provider)
}
=> false
;
"not the latest nomination reference"
)]
#[allow(clippy::unwrap_used)]
fn contest_delegation(
doc_gen: impl Fn(&mut TestCatalystProvider) -> anyhow::Result<CatalystSignedDocument>
) -> bool {
let mut provider = TestCatalystProvider::default();
let doc = doc_gen(&mut provider).unwrap();

let mut validator = Validator::new();
validator
.extend_rules_per_document(doc_types::CONTEST_DELEGATION.clone(), ContestDelegationRule);

let is_valid = validator.validate(&doc, &provider).unwrap();
assert_eq!(is_valid, !doc.report().is_problematic());
println!("{:?}", doc.report());

// Generate similar `CatalystSignedDocument` instance to have a clean internal problem
// report
let doc = doc_gen(&mut provider).unwrap();
let contest_delegation = ContestDelegation::new(&doc, &provider).unwrap();
assert_eq!(is_valid, !contest_delegation.report().is_problematic());
println!("{:?}", contest_delegation.report());

is_valid
}
1 change: 0 additions & 1 deletion rust/signed_doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ strum = { version = "0.27.1", features = ["derive"] }
strum_macros = { version = "0.27.1" }
clap = { version = "4.5.23", features = ["derive", "env"] }
jsonpath-rust = "0.7.5"
futures = "0.3.31"
ed25519-bip32 = "0.4.1" # used by the `mk_signed_doc` cli tool
tracing = "0.1.40"
thiserror = "2.0.11"
Expand Down
Loading
Loading