use std::cmp::Ordering;
use sha2::{Digest, Sha256};
use super::super::verify::verify_sparse_artifact;
use super::super::wire::CollapseArtifact;
use super::model::{
CollapsePortfolio, CollapsePortfolioArtifact, CollapsePortfolioArtifactEntry,
CollapsePortfolioDecodeLimits, CollapsePortfolioLimits, CollapsePortfolioObjective,
};
use super::selection::{portfolio_error, score_graph, validate_profile, validate_request};
use super::wire::{
PORTFOLIO_MAGIC, PORTFOLIO_VERSION, decode_portfolio_digest, decode_portfolio_payload,
encode_portfolio_entry, encode_portfolio_objective, put_portfolio_u16, put_portfolio_usize,
};
use crate::{Result, SparseDistanceMatrix};
impl CollapsePortfolioArtifact {
pub fn from_portfolio(
portfolio: &CollapsePortfolio,
limits: CollapsePortfolioLimits,
) -> Result<Self> {
let mut entries = Vec::with_capacity(portfolio.entries.len());
for entry in &portfolio.entries {
let artifact = CollapseArtifact::from_result(&entry.result)
.map_err(|error| portfolio_error(error.to_string()))?;
entries.push(CollapsePortfolioArtifactEntry {
candidate: entry.candidate,
score: entry.score.clone(),
artifact,
});
}
let mut artifact = Self {
objective: portfolio.objective,
entries,
selected: portfolio.selected,
digest: [0; 32],
};
artifact.validate_structure(limits)?;
artifact.digest = artifact.compute_digest()?;
Ok(artifact)
}
pub fn objective(&self) -> CollapsePortfolioObjective {
self.objective
}
pub fn entries(&self) -> &[CollapsePortfolioArtifactEntry] {
&self.entries
}
pub fn selected_index(&self) -> usize {
self.selected
}
pub fn selected(&self) -> &CollapsePortfolioArtifactEntry {
&self.entries[self.selected]
}
pub fn digest(&self) -> &[u8; 32] {
&self.digest
}
pub fn verify_sparse(
&self,
input: &SparseDistanceMatrix,
threshold: Option<f64>,
limits: CollapsePortfolioLimits,
) -> Result<()> {
self.validate_structure(limits)?;
if self.compute_digest()? != self.digest {
return Err(portfolio_error(
"portfolio digest does not match its payload",
));
}
for entry in &self.entries {
verify_sparse_artifact(input, threshold, &entry.artifact)
.map_err(|error| portfolio_error(format!("candidate artifact failed: {error}")))?;
}
Ok(())
}
pub fn encode(
&self,
portfolio_limits: CollapsePortfolioLimits,
decode_limits: CollapsePortfolioDecodeLimits,
) -> Result<Vec<u8>> {
self.validate_structure(portfolio_limits)?;
if self.compute_digest()? != self.digest {
return Err(portfolio_error(
"portfolio digest does not match its payload",
));
}
let mut bytes = self.encode_payload()?;
bytes.extend_from_slice(&self.digest);
if bytes.len() > decode_limits.max_bytes {
return Err(portfolio_error("portfolio exceeds its byte limit"));
}
Ok(bytes)
}
pub fn decode(
bytes: &[u8],
portfolio_limits: CollapsePortfolioLimits,
decode_limits: CollapsePortfolioDecodeLimits,
) -> Result<Self> {
let (payload, digest) = decode_portfolio_digest(bytes, decode_limits.max_bytes)?;
let artifact = decode_portfolio_payload(payload, digest, portfolio_limits, decode_limits)?;
artifact.validate_structure(portfolio_limits)?;
Ok(artifact)
}
fn validate_structure(&self, limits: CollapsePortfolioLimits) -> Result<()> {
let candidates = self
.entries
.iter()
.map(|entry| entry.candidate)
.collect::<Vec<_>>();
validate_request(&candidates, self.objective, limits)?;
if self.selected >= self.entries.len() {
return Err(portfolio_error(
"selected candidate is outside the portfolio",
));
}
let first = &self.entries[0].artifact;
for entry in &self.entries {
validate_profile(entry.candidate, entry.artifact.certificate())?;
validate_common_input(first, &entry.artifact)?;
let score = score_graph(entry.artifact.matrix(), self.objective, limits)?;
if score != entry.score {
return Err(portfolio_error("candidate score does not match its graph"));
}
}
if select_artifact_entry(&self.entries) != self.selected {
return Err(portfolio_error(
"selected candidate is not the exact minimum",
));
}
Ok(())
}
fn encode_payload(&self) -> Result<Vec<u8>> {
let mut output = Vec::new();
output.extend_from_slice(PORTFOLIO_MAGIC);
put_portfolio_u16(&mut output, PORTFOLIO_VERSION);
encode_portfolio_objective(&mut output, self.objective)?;
put_portfolio_usize(&mut output, self.entries.len(), "candidate count")?;
put_portfolio_usize(&mut output, self.selected, "selected candidate")?;
for entry in &self.entries {
encode_portfolio_entry(&mut output, entry)?;
}
Ok(output)
}
pub(super) fn compute_digest(&self) -> Result<[u8; 32]> {
Ok(Sha256::digest(self.encode_payload()?).into())
}
}
fn validate_common_input(reference: &CollapseArtifact, candidate: &CollapseArtifact) -> Result<()> {
if reference.input_digest() != candidate.input_digest()
|| !same_optional_float(
reference.certificate().requested_threshold(),
candidate.certificate().requested_threshold(),
)
{
return Err(portfolio_error(
"candidate artifacts do not bind one thresholded input",
));
}
Ok(())
}
fn same_optional_float(left: Option<f64>, right: Option<f64>) -> bool {
match (left, right) {
(None, None) => true,
(Some(left), Some(right)) => left.to_bits() == right.to_bits(),
_ => false,
}
}
fn select_artifact_entry(entries: &[CollapsePortfolioArtifactEntry]) -> usize {
let mut selected = 0;
for index in 1..entries.len() {
if entries[index].score.compare(&entries[selected].score) == Ordering::Less {
selected = index;
}
}
selected
}