use super::queue_consumer::TriggerSender;
use super::workflow::incoming_dht_ops_workflow::incoming_dht_ops_workflow;
use crate::conductor::space::Space;
pub use error::*;
pub use holo_hash::*;
use holochain_keystore::AgentPubKeyExt;
pub use holochain_state::source_chain::SourceChainError;
pub use holochain_state::source_chain::SourceChainResult;
use holochain_types::prelude::*;
use std::sync::Arc;
mod error;
#[cfg(test)]
mod tests;
pub const MAX_ENTRY_SIZE: usize = ENTRY_SIZE_LIMIT;
pub const MAX_TAG_SIZE: usize = 1000;
pub async fn verify_action_signature(sig: &Signature, action: &Action) -> SysValidationResult<()> {
if action.signer().verify_signature(sig, action).await? {
Ok(())
} else {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::CounterfeitAction((*sig).clone(), Box::new(action.clone())),
))
}
}
pub async fn verify_warrant_signature(
warrant_op: &holochain_types::warrant::WarrantOp,
) -> SysValidationResult<()> {
if warrant_op
.author
.verify_signature(warrant_op.signature(), warrant_op.warrant().clone())
.await?
{
Ok(())
} else {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::CounterfeitWarrant(Box::new(warrant_op.warrant().clone())),
))
}
}
pub fn check_countersigning_session_data_contains_action(
entry_hash: EntryHash,
session_data: &CounterSigningSessionData,
action: &Action,
) -> SysValidationResult<()> {
let action_is_in_session = build_action_set(session_data, entry_hash)
.map_err(SysValidationError::from)?
.iter()
.any(|session_action| session_action == action);
if !action_is_in_session {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::ActionNotInCounterSigningSession(
Box::new(session_data.to_owned()),
Box::new(action.clone()),
),
))
} else {
Ok(())
}
}
pub async fn check_countersigning_preflight_response_signature(
preflight_response: &PreflightResponse,
) -> SysValidationResult<()> {
let signature_is_valid = preflight_response
.request()
.signing_agents
.get(*preflight_response.agent_state().agent_index() as usize)
.ok_or_else(|| {
SysValidationError::ValidationOutcome(ValidationOutcome::PreflightResponseSignature(
Box::new((*preflight_response).clone()),
))
})?
.0
.verify_signature_raw(
preflight_response.signature(),
preflight_response
.encode_for_signature()
.map_err(|_| {
SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature(Box::new(
(*preflight_response).clone(),
)),
)
})?
.into(),
)
.await?;
if signature_is_valid {
Ok(())
} else {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature(Box::new((*preflight_response).clone())),
))
}
}
pub async fn check_countersigning_session_data(
entry_hash: EntryHash,
session_data: &CounterSigningSessionData,
action: &Action,
) -> SysValidationResult<()> {
session_data.check_integrity()?;
check_countersigning_session_data_contains_action(entry_hash, session_data, action)?;
let tasks: Vec<_> = session_data
.responses()
.iter()
.map(|(response, signature)| async move {
let preflight_response = PreflightResponse::try_new(
session_data.preflight_request().clone(),
response.clone(),
signature.clone(),
)?;
check_countersigning_preflight_response_signature(&preflight_response).await
})
.collect();
let results: Vec<SysValidationResult<()>> = futures::future::join_all(tasks).await;
let results: SysValidationResult<()> = results.into_iter().collect();
match results {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
pub fn check_prev_action(action: &Action) -> SysValidationResult<()> {
let is_dna = matches!(action.data, ActionData::Dna(_));
let has_prev = action.prev_action().is_some();
let is_first = action.action_seq() == 0;
#[allow(clippy::collapsible_else_if)]
if is_first {
if is_dna && !has_prev {
Ok(())
} else {
Err(PrevActionErrorKind::InvalidRoot)
}
} else {
if !is_dna && has_prev {
Ok(())
} else {
Err(PrevActionErrorKind::MissingPrev)
}
}
.map_err(|e| ValidationOutcome::PrevActionError((e, action.clone()).into()).into())
}
pub fn check_valid_if_dna(action: &Action, dna_hash: &DnaHash) -> SysValidationResult<()> {
match &action.data {
ActionData::Dna(DnaData {
dna_hash: action_dna_hash,
}) => {
if action_dna_hash != dna_hash {
Err(ValidationOutcome::WrongDna(action_dna_hash.clone(), dna_hash.clone()).into())
} else {
Ok(())
}
}
_ => Ok(()),
}
}
pub fn check_agent_validation_pkg_predecessor(
action: &Action,
prev_action: &Action,
) -> SysValidationResult<()> {
let is_agent_pubkey_entry_action = |a: &Action| {
matches!(
&a.data,
ActionData::Create(CreateData {
entry_type: EntryType::AgentPubKey,
..
}) | ActionData::Update(UpdateData {
entry_type: EntryType::AgentPubKey,
..
})
)
};
let prev_is_avp = matches!(prev_action.data, ActionData::AgentValidationPkg(_));
let action_is_agent_pubkey_entry = is_agent_pubkey_entry_action(action);
let maybe_error = match (prev_is_avp, action_is_agent_pubkey_entry) {
(true, true) => None,
(true, false) => Some(
"Every AgentValidationPkg must be followed by a Create or Update for an AgentPubKey",
),
(false, true) => Some(
"Every Create or Update for an AgentPubKey must be preceded by an AgentValidationPkg",
),
(false, false) => None,
};
if let Some(error) = maybe_error {
Err(PrevActionErrorKind::InvalidSuccessor(
error.to_string(),
Box::new((prev_action.clone(), action.clone())),
))
.map_err(|e| ValidationOutcome::PrevActionError((e, action.clone()).into()).into())
} else {
Ok(())
}
}
pub fn check_prev_author(action: &Action, prev_action: &Action) -> SysValidationResult<()> {
let a1 = prev_action.author().clone();
let a2 = action.author();
if a1 == *a2 {
Ok(())
} else {
Err(PrevActionErrorKind::Author(a1, a2.clone()))
.map_err(|e| ValidationOutcome::PrevActionError((e, action.clone()).into()).into())
}
}
pub fn check_prev_timestamp(action: &Action, prev_action: &Action) -> SysValidationResult<()> {
let t1 = prev_action.timestamp();
let t2 = action.timestamp();
if t2 >= t1 {
Ok(())
} else {
Err(PrevActionErrorKind::Timestamp(t1, t2))
.map_err(|e| ValidationOutcome::PrevActionError((e, action.clone()).into()).into())
}
}
pub fn check_prev_seq(action: &Action, prev_action: &Action) -> SysValidationResult<()> {
let action_seq = action.action_seq();
let prev_seq = prev_action.action_seq();
if action_seq > 0 && prev_seq == action_seq - 1 {
Ok(())
} else {
Err(PrevActionErrorKind::InvalidSeq(action_seq, prev_seq))
.map_err(|e| ValidationOutcome::PrevActionError((e, action.clone()).into()).into())
}
}
pub fn check_entry_type(entry_type: &EntryType, entry: &Entry) -> SysValidationResult<()> {
entry_type_matches(entry_type, entry)
.then_some(())
.ok_or_else(|| ValidationOutcome::EntryTypeMismatch.into())
}
pub fn check_entry_visibility(op: &ChainOp) -> SysValidationResult<()> {
use EntryVisibility::*;
use OpEntry::*;
let action = op.signed_action().data();
let err = |reason: &str| {
Err(ValidationOutcome::MalformedDhtOp(
Box::new(action.clone()),
op.op_type(),
reason.to_string(),
)
.into())
};
match (action.entry_visibility(), op.op_entry()) {
(_, None) => Ok(()),
(Some(Public), Some(Present(_))) => Ok(()),
(Some(Private), _) if matches!(op, ChainOp::CreateEntry(..)) => {
Err(ValidationOutcome::PrivateEntryLeaked.into())
}
(Some(Private), Some(Hidden)) => Ok(()),
(Some(Private), Some(ActionOnly)) => Ok(()),
(Some(Public), Some(Hidden)) => err("OpEntry::Hidden is only for Private entry type"),
(Some(Private), Some(Present(_))) => Err(ValidationOutcome::PrivateEntryLeaked.into()),
(Some(Public), Some(ActionOnly)) => {
if action.entry_type() == Some(&EntryType::AgentPubKey) {
Ok(())
} else {
err("Op has public entry type but is missing its data")
}
}
(None, Some(ActionOnly)) => Ok(()),
(None, Some(Present(_) | Hidden)) => {
err("Entry must be absent for action with no entry type")
}
}
}
pub fn check_entry_hash(hash: &EntryHash, entry: &Entry) -> SysValidationResult<()> {
if *hash == EntryHash::with_data_sync(entry) {
Ok(())
} else {
Err(ValidationOutcome::EntryHash.into())
}
}
pub fn check_new_entry_action(action: &Action) -> SysValidationResult<()> {
match action.data {
ActionData::Create(_) | ActionData::Update(_) => Ok(()),
_ => Err(ValidationOutcome::NotNewEntry(Box::new(action.clone())).into()),
}
}
pub fn check_entry_size(entry: &Entry) -> SysValidationResult<()> {
match entry {
Entry::App(bytes) | Entry::CounterSign(_, bytes) => {
let size = std::mem::size_of_val(&bytes.bytes()[..]);
if size <= MAX_ENTRY_SIZE {
Ok(())
} else {
Err(ValidationOutcome::EntryTooLarge(size).into())
}
}
_ => {
Ok(())
}
}
}
pub fn check_tag_size(tag: &LinkTag) -> SysValidationResult<()> {
let size = std::mem::size_of_val(&tag.0[..]);
if size <= MAX_TAG_SIZE {
Ok(())
} else {
Err(ValidationOutcome::TagTooLarge(size).into())
}
}
pub fn check_update_reference(
update: &UpdateData,
original_entry_action: &Action,
) -> SysValidationResult<()> {
let (entry_hash, entry_type) = original_entry_action
.entry_data()
.ok_or_else(|| ValidationOutcome::NotNewEntry(Box::new(original_entry_action.clone())))?;
if update.entry_type != *entry_type {
return Err(ValidationOutcome::UpdateTypeMismatch(
entry_type.clone(),
update.entry_type.clone(),
)
.into());
}
if update.original_entry_address != *entry_hash {
return Err(ValidationOutcome::UpdateHashMismatch(
entry_hash.clone(),
update.original_entry_address.clone(),
)
.into());
}
Ok(())
}
#[async_trait::async_trait]
#[cfg_attr(test, mockall::automock)]
pub trait DhtOpSender {
async fn send_op(&self, op: DhtOp) -> SysValidationResult<()>;
async fn send_store_record(&self, record: Record) -> SysValidationResult<()>;
async fn send_store_entry(&self, record: Record) -> SysValidationResult<()>;
async fn send_register_add_link(&self, record: Record) -> SysValidationResult<()>;
async fn send_register_agent_activity(&self, record: Record) -> SysValidationResult<()>;
}
#[derive(derive_more::Constructor, Clone)]
pub struct IncomingDhtOpSender {
space: Arc<Space>,
sys_validation_trigger: TriggerSender,
}
#[async_trait::async_trait]
impl DhtOpSender for IncomingDhtOpSender {
async fn send_op(&self, op: DhtOp) -> SysValidationResult<()> {
let ops = vec![(op, false)];
Ok(incoming_dht_ops_workflow(
self.space.as_ref().clone(),
self.sys_validation_trigger.clone(),
ops,
)
.await
.map_err(Box::new)?)
}
async fn send_store_record(&self, record: Record) -> SysValidationResult<()> {
self.send_op(make_store_record(record).into()).await
}
async fn send_store_entry(&self, record: Record) -> SysValidationResult<()> {
let is_public_entry = record
.action()
.entry_type()
.is_some_and(|et| matches!(et.visibility(), EntryVisibility::Public));
if is_public_entry {
if let Some(op) = make_store_entry(record) {
self.send_op(op.into()).await?;
}
}
Ok(())
}
async fn send_register_add_link(&self, record: Record) -> SysValidationResult<()> {
if let Some(op) = make_register_add_link(record) {
self.send_op(op.into()).await?;
}
Ok(())
}
async fn send_register_agent_activity(&self, record: Record) -> SysValidationResult<()> {
self.send_op(make_register_agent_activity(record).into())
.await
}
}
fn record_entry_to_op_entry(entry: RecordEntry) -> OpEntry {
match entry {
RecordEntry::Present(e) => OpEntry::Present(e),
RecordEntry::Hidden => OpEntry::Hidden,
RecordEntry::NA | RecordEntry::NotStored => OpEntry::ActionOnly,
}
}
fn make_store_record(record: Record) -> ChainOp {
let (shh, record_entry) = record.privatized().0.into_inner();
let (hashed, signature) = shh.into_inner();
let action = hashed.into_content();
ChainOp::CreateRecord(
SignedAction::new(action, signature),
record_entry_to_op_entry(record_entry),
)
}
fn make_store_entry(record: Record) -> Option<ChainOp> {
let (shh, record_entry) = record.into_inner();
let (hashed, signature) = shh.into_inner();
let action = hashed.into_content();
let entry = record_entry.into_option()?;
if !matches!(action.data, ActionData::Create(_) | ActionData::Update(_)) {
return None;
}
Some(ChainOp::CreateEntry(
SignedAction::new(action, signature),
OpEntry::Present(entry),
))
}
fn make_register_add_link(record: Record) -> Option<ChainOp> {
let (shh, _) = record.into_inner();
let (hashed, signature) = shh.into_inner();
let action = hashed.into_content();
if !matches!(action.data, ActionData::CreateLink(_)) {
return None;
}
Some(ChainOp::CreateLink(SignedAction::new(action, signature)))
}
fn make_register_agent_activity(record: Record) -> ChainOp {
let (shh, _) = record.into_inner();
let (hashed, signature) = shh.into_inner();
let action = hashed.into_content();
ChainOp::AgentActivity(SignedAction::new(action, signature))
}
#[cfg(test)]
mod test {
use super::check_countersigning_preflight_response_signature;
use crate::core::sys_validate::error::SysValidationError;
use crate::core::ValidationOutcome;
use crate::prelude::EntryTypeFixturator;
use crate::prelude::{ActionBase, CounterSigningAgentState, CounterSigningSessionTimes};
use fixt::fixt;
use hdk::prelude::{PreflightBytes, Signature, SIGNATURE_BYTES};
use holo_hash::fixt::ActionHashFixturator;
use holo_hash::fixt::EntryHashFixturator;
use holochain_keystore::AgentPubKeyExt;
use holochain_timestamp::Timestamp;
use holochain_types::prelude::PreflightRequest;
use holochain_zome_types::prelude::{CreateBase, PreflightResponse};
use matches::assert_matches;
use std::time::Duration;
#[tokio::test(flavor = "multi_thread")]
async fn test_check_countersigning_preflight_response_signature() {
let keystore = holochain_keystore::test_keystore();
let agent_1 = keystore.new_sign_keypair_random().await.unwrap();
let agent_2 = keystore.new_sign_keypair_random().await.unwrap();
let request = PreflightRequest::try_new(
fixt!(EntryHash),
vec![(agent_1.clone(), vec![]), (agent_2, vec![])],
vec![],
0,
false,
CounterSigningSessionTimes::try_new(
Timestamp::now(),
(Timestamp::now() + Duration::from_secs(30)).unwrap(),
)
.unwrap(),
ActionBase::Create(CreateBase::new(fixt!(EntryType))),
PreflightBytes(vec![1, 2, 3]),
)
.unwrap();
let agent_state = [
CounterSigningAgentState::new(0, fixt!(ActionHash), 100),
CounterSigningAgentState::new(1, fixt!(ActionHash), 50),
];
let preflight_response = PreflightResponse::try_new(
request.clone(),
agent_state[0].clone(),
Signature(vec![0; SIGNATURE_BYTES].try_into().unwrap()),
)
.unwrap();
assert_matches!(
check_countersigning_preflight_response_signature(&preflight_response).await,
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature(_)
))
);
let sig_data =
PreflightResponse::encode_fields_for_signature(&request, &agent_state[0]).unwrap();
let signature = agent_1.sign_raw(&keystore, sig_data.into()).await.unwrap();
let preflight_response =
PreflightResponse::try_new(request, agent_state[0].clone(), signature).unwrap();
check_countersigning_preflight_response_signature(&preflight_response)
.await
.unwrap();
}
}