#![forbid(unsafe_code)]
#![deny(missing_docs)]
use core::fmt;
use hop_core::{poly_matrix_eval_checked, CompanionMatrix, Field, Mat};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const ACTION_HASH_DOMAIN: &[u8] = b"HOP-ACTION\0\x02";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Action {
pub g_coeffs: Vec<u64>,
pub nonce: u64,
#[serde(default)]
pub mask_row0: Option<Vec<u64>>,
}
impl Action {
pub fn try_new(
g_coeffs: Vec<u64>,
nonce: u64,
mask_row0: Option<Vec<u64>>,
dimension: usize,
) -> Result<Self, ActionError> {
let action = Self {
g_coeffs,
nonce,
mask_row0,
};
action.validate_for_dimension(dimension)?;
Ok(action)
}
pub fn validate_for_dimension(&self, dimension: usize) -> Result<(), ActionError> {
if dimension == 0 {
return Err(ActionError::ZeroDimension);
}
self.validate()?;
if let Some(mask) = &self.mask_row0 {
if mask.len() != dimension {
return Err(ActionError::MaskDimensionMismatch {
expected: dimension,
actual: mask.len(),
});
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), ActionError> {
if self.g_coeffs.is_empty() {
return Err(ActionError::EmptyPolynomial);
}
validate_canonical_values(&self.g_coeffs, "polynomial")?;
if let Some(mask) = &self.mask_row0 {
if mask.is_empty() {
return Err(ActionError::EmptyMask);
}
validate_canonical_values(mask, "mask")?;
}
Ok(())
}
pub fn hash(&self) -> [u8; 32] {
hash_action(self)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionSeq(pub Vec<Action>);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capability {
pub max_degree: usize,
pub topic: [u8; 32],
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
pub subject: [u8; 32],
pub scope: Capability,
pub sig: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ActionError {
ZeroDimension,
EmptyPolynomial,
EmptyMask,
NonCanonicalFieldValue {
field: &'static str,
index: usize,
value: u64,
},
MaskDimensionMismatch {
expected: usize,
actual: usize,
},
StateDimensionMismatch {
expected: usize,
actual: usize,
},
InvalidTransform(hop_core::CoreError),
}
impl fmt::Display for ActionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ZeroDimension => f.write_str("an action dimension must be nonzero"),
Self::EmptyPolynomial => f.write_str("an action polynomial must not be empty"),
Self::EmptyMask => f.write_str("a present action mask must not be empty"),
Self::NonCanonicalFieldValue {
field,
index,
value,
} => write!(
f,
"{field} value at index {index} is not canonical: {value} >= {}",
Field::MOD
),
Self::MaskDimensionMismatch { expected, actual } => write!(
f,
"mask dimension mismatch: expected {expected}, got {actual}"
),
Self::StateDimensionMismatch { expected, actual } => write!(
f,
"state dimension mismatch: expected {expected}, got {actual}"
),
Self::InvalidTransform(error) => write!(f, "invalid action transform: {error}"),
}
}
}
impl std::error::Error for ActionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidTransform(error) => Some(error),
_ => None,
}
}
}
pub fn hash_action(action: &Action) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(ACTION_HASH_DOMAIN);
hasher.update(action.nonce.to_le_bytes());
update_values(&mut hasher, &action.g_coeffs);
match &action.mask_row0 {
None => hasher.update([0]),
Some(mask) => {
hasher.update([1]);
update_values(&mut hasher, mask);
}
}
hasher.finalize().into()
}
pub fn action_transform(companion: &CompanionMatrix, action: &Action) -> Result<Mat, ActionError> {
action.validate_for_dimension(companion.dimension())?;
poly_matrix_eval_checked(&action.g_coeffs, companion.matrix())
.map_err(ActionError::InvalidTransform)
}
pub fn apply_action(
companion: &CompanionMatrix,
state: &[Field],
action: &Action,
) -> Result<Vec<Field>, ActionError> {
let dimension = companion.dimension();
if state.len() != dimension {
return Err(ActionError::StateDimensionMismatch {
expected: dimension,
actual: state.len(),
});
}
action.validate_for_dimension(dimension)?;
let mut result = vec![Field::zero(); dimension];
for coefficient in action.g_coeffs.iter().rev() {
result = companion
.try_apply(&result)
.map_err(ActionError::InvalidTransform)?;
let scalar = Field::from(*coefficient);
for (output, input) in result.iter_mut().zip(state) {
*output += scalar * *input;
}
}
Ok(result)
}
fn validate_canonical_values(values: &[u64], field: &'static str) -> Result<(), ActionError> {
if let Some((index, &value)) = values
.iter()
.enumerate()
.find(|(_, value)| **value >= Field::MOD)
{
return Err(ActionError::NonCanonicalFieldValue {
field,
index,
value,
});
}
Ok(())
}
fn update_values(hasher: &mut Sha256, values: &[u64]) {
hasher.update((values.len() as u128).to_le_bytes());
for value in values {
hasher.update(value.to_le_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn companion() -> CompanionMatrix {
CompanionMatrix::try_from_coeffs(&[4, 3, 2]).unwrap()
}
#[test]
fn hash_framing_distinguishes_coefficients_from_mask() {
let coefficients_then_mask = Action {
g_coeffs: vec![1],
nonce: 7,
mask_row0: Some(vec![2, 0, 0]),
};
let coefficients_only = Action {
g_coeffs: vec![1, 2, 0, 0],
nonce: 7,
mask_row0: None,
};
assert_ne!(
hash_action(&coefficients_then_mask),
hash_action(&coefficients_only)
);
}
#[test]
fn action_applies_polynomial_transform() {
let c = companion();
let state = vec![Field::one(), Field::zero(), Field::zero()];
let identity = Action::try_new(vec![1], 0, None, 3).unwrap();
let transition = Action::try_new(vec![0, 1], 1, None, 3).unwrap();
assert_eq!(apply_action(&c, &state, &identity).unwrap(), state);
assert_eq!(
apply_action(&c, &state, &transition).unwrap(),
c.try_apply(&state).unwrap()
);
let polynomial = Action::try_new(vec![7, 11, 13, 17], 2, None, 3).unwrap();
let dense = action_transform(&c, &polynomial).unwrap();
let expected: Vec<Field> = dense
.iter()
.map(|row| {
row.iter()
.zip(&state)
.fold(Field::zero(), |sum, (left, right)| sum + (*left * *right))
})
.collect();
assert_eq!(apply_action(&c, &state, &polynomial).unwrap(), expected);
}
#[test]
fn rejects_ambiguous_or_noncanonical_wire_values() {
assert_eq!(
Action::try_new(Vec::new(), 0, None, 3),
Err(ActionError::EmptyPolynomial)
);
assert_eq!(
Action::try_new(vec![1], 0, None, 0),
Err(ActionError::ZeroDimension)
);
assert_eq!(
Action::try_new(vec![1], 0, Some(Vec::new()), 3),
Err(ActionError::EmptyMask)
);
assert!(matches!(
Action::try_new(vec![Field::MOD], 0, None, 3),
Err(ActionError::NonCanonicalFieldValue { .. })
));
assert!(matches!(
Action::try_new(vec![1], 0, Some(vec![1]), 3),
Err(ActionError::MaskDimensionMismatch { .. })
));
}
}