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: &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: NewEntryActionRef<'_>,
) -> SysValidationResult<()> {
let weight = match action {
NewEntryActionRef::Create(h) => h.weight.clone(),
NewEntryActionRef::Update(h) => h.weight.clone(),
};
let action_is_in_session = session_data
.build_action_set(entry_hash, weight)
.map_err(SysValidationError::from)?
.iter()
.any(|session_action| match (&action, session_action) {
(NewEntryActionRef::Create(create), Action::Create(session_create)) => {
create == &session_create
}
(NewEntryActionRef::Update(update), Action::Update(session_update)) => {
update == &session_update
}
_ => false,
});
if !action_is_in_session {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::ActionNotInCounterSigningSession(
Box::new(session_data.to_owned()),
Box::new(action.to_new_entry_action()),
),
))
} 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: NewEntryActionRef<'_>,
) -> 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, Action::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 {
Action::Dna(a) => {
if a.hash != *dna_hash {
Err(ValidationOutcome::WrongDna(a.hash.clone(), dna_hash.clone()).into())
} else {
Ok(())
}
}
_ => Ok(()),
}
}
pub fn check_agent_validation_pkg_predecessor(
action: &Action,
prev_action: &Action,
) -> SysValidationResult<()> {
let maybe_error = match (prev_action, action) {
(
Action::AgentValidationPkg(AgentValidationPkg { .. }),
Action::Create(Create {
entry_type: EntryType::AgentPubKey,
..
})
| Action::Update(Update {
entry_type: EntryType::AgentPubKey,
..
}),
) => None,
(Action::AgentValidationPkg(AgentValidationPkg { .. }), _) => Some(
"Every AgentValidationPkg must be followed by a Create or Update for an AgentPubKey",
),
(
_,
Action::Create(Create {
entry_type: EntryType::AgentPubKey,
..
})
| Action::Update(Update {
entry_type: EntryType::AgentPubKey,
..
}),
) => Some(
"Every Create or Update for an AgentPubKey must be preceded by an AgentValidationPkg",
),
_ => 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 RecordEntry::*;
let err = |reason: &str| {
Err(ValidationOutcome::MalformedDhtOp(
Box::new(op.action()),
op.get_type(),
reason.to_string(),
)
.into())
};
match (op.action().entry_type().map(|t| t.visibility()), op.entry()) {
(Some(Public), Present(_)) => Ok(()),
(Some(Private), Hidden) => Ok(()),
(Some(Private), NotStored) => Ok(()),
(Some(Public), Hidden) => err("RecordEntry::Hidden is only for Private entry type"),
(Some(_), NA) => err("There is action entry data but the entry itself is N/A"),
(Some(Private), Present(_)) => Err(ValidationOutcome::PrivateEntryLeaked.into()),
(Some(Public), NotStored) => {
if op.get_type() == ChainOpType::RegisterAgentActivity
|| op.action().entry_type() == Some(&EntryType::AgentPubKey)
{
Ok(())
} else {
err("Op has public entry type but is missing its data")
}
}
(None, NA) => Ok(()),
(None, _) => err("Entry must be N/A 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 {
Action::Create(_) | Action::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: &Update,
original_entry_action: &NewEntryActionRef<'_>,
) -> SysValidationResult<()> {
if update.entry_type != *original_entry_action.entry_type() {
return Err(ValidationOutcome::UpdateTypeMismatch(
original_entry_action.entry_type().clone(),
update.entry_type.clone(),
)
.into());
}
if update.original_entry_address != *original_entry_action.entry_hash() {
return Err(ValidationOutcome::UpdateHashMismatch(
original_entry_action.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];
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 make_store_record(record: Record) -> ChainOp {
let (shh, record_entry) = record.privatized().0.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content();
ChainOp::StoreRecord(signature, action, record_entry)
}
fn make_store_entry(record: Record) -> Option<ChainOp> {
let (shh, record_entry) = record.into_inner();
let (action, signature) = shh.into_inner();
let entry_box = record_entry.into_option()?;
let action = action.into_content().try_into().ok()?;
let op = ChainOp::StoreEntry(signature, action, entry_box);
Some(op)
}
fn make_register_add_link(record: Record) -> Option<ChainOp> {
let (shh, _) = record.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content().try_into().ok()?;
let op = ChainOp::RegisterAddLink(signature, action);
Some(op)
}
fn make_register_agent_activity(record: Record) -> ChainOp {
let (shh, _) = record.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content();
ChainOp::RegisterAgentActivity(signature, action)
}
#[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::countersigning::PreflightResponse;
use holochain_zome_types::prelude::CreateBase;
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();
}
}