use super::queue_consumer::TriggerSender;
use super::ribosome::RibosomeT;
use super::workflow::incoming_dht_ops_workflow::incoming_dht_ops_workflow;
use super::workflow::sys_validation_workflow::SysValidationWorkspace;
use crate::conductor::entry_def_store::get_entry_def;
use crate::conductor::space::Space;
use crate::conductor::Conductor;
use holochain_cascade::Cascade;
use holochain_cascade::CascadeSource;
use holochain_keystore::AgentPubKeyExt;
use holochain_types::prelude::*;
use holochain_zome_types::countersigning::CounterSigningSessionData;
use std::convert::TryInto;
use std::sync::Arc;
pub use error::*;
pub use holo_hash::*;
pub use holochain_state::source_chain::SourceChainError;
pub use holochain_state::source_chain::SourceChainResult;
pub use holochain_zome_types::ActionHashed;
pub use holochain_zome_types::Timestamp;
#[allow(missing_docs)]
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.author().verify_signature(sig, action).await {
Ok(())
} else {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::Counterfeit((*sig).clone(), (*action).clone()),
))
}
}
pub async fn author_key_is_valid(_author: &AgentPubKey) -> SysValidationResult<()> {
Ok(())
}
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(
session_data.to_owned(),
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(
(*preflight_response).clone(),
))
})?
.0
.verify_signature_raw(
preflight_response.signature(),
preflight_response
.encode_for_signature()
.map_err(|_| {
SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature(
(*preflight_response).clone(),
),
)
})?
.into(),
)
.await;
if signature_is_valid {
Ok(())
} else {
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature((*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(PrevActionError::InvalidRoot)
}
} else {
if !is_dna && has_prev {
Ok(())
} else {
Err(PrevActionError::MissingPrev)
}
}
.map_err(|e| ValidationOutcome::from(e).into())
}
pub fn check_valid_if_dna(action: &Action, dna_def: &DnaDefHashed) -> SysValidationResult<()> {
match action {
Action::Dna(a) => {
let dna_hash = dna_def.as_hash();
if a.hash != *dna_hash {
Err(ValidationOutcome::WrongDna(a.hash.clone(), dna_hash.clone()).into())
} else if action.timestamp() < dna_def.modifiers.origin_time {
Err(PrevActionError::InvalidRootOriginTime)
.map_err(|e| ValidationOutcome::from(e).into())
} else {
Ok(())
}
}
_ => Ok(()),
}
}
pub async fn check_chain_rollback(
action: &Action,
workspace: &SysValidationWorkspace,
) -> SysValidationResult<()> {
let empty = workspace.action_seq_is_empty(action).await?;
if empty {
Ok(())
} else {
tracing::error!(
"Chain rollback detected at position {} for agent {:?} from action {:?}",
action.action_seq(),
action.author(),
action,
);
Ok(())
}
}
pub async fn check_spam(_action: &Action) -> SysValidationResult<()> {
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 { .. }) | Action::Update(Update { .. }),
) => 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(PrevActionError::InvalidSuccessor(
error.to_string(),
Box::new((prev_action.clone(), action.clone())),
))
.map_err(|e| ValidationOutcome::from(e).into())
} else {
Ok(())
}
}
pub fn check_prev_author(action: &Action, prev_action: &Action) -> SysValidationResult<()> {
let a1: AgentPubKey = if let Action::Update(
u @ Update {
entry_type: EntryType::AgentPubKey,
..
},
) = prev_action
{
#[cfg(feature = "dpki")]
{
u.entry_hash.clone().into()
}
#[cfg(not(feature = "dpki"))]
{
u.author.clone()
}
} else {
prev_action.author().clone()
};
let a2 = action.author();
if a1 == *a2 {
Ok(())
} else {
Err(PrevActionError::Author(a1, a2.clone())).map_err(|e| ValidationOutcome::from(e).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(PrevActionError::Timestamp(t1, t2)).map_err(|e| ValidationOutcome::from(e).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(PrevActionError::InvalidSeq(action_seq, prev_seq))
.map_err(|e| ValidationOutcome::from(e).into())
}
}
pub fn check_entry_type(entry_type: &EntryType, entry: &Entry) -> SysValidationResult<()> {
match (entry_type, entry) {
(EntryType::AgentPubKey, Entry::Agent(_)) => Ok(()),
(EntryType::App(_), Entry::App(_)) => Ok(()),
(EntryType::App(_), Entry::CounterSign(_, _)) => Ok(()),
(EntryType::CapClaim, Entry::CapClaim(_)) => Ok(()),
(EntryType::CapGrant, Entry::CapGrant(_)) => Ok(()),
_ => Err(ValidationOutcome::EntryType.into()),
}
}
pub async fn check_app_entry_def(
dna_hash: &DnaHash,
entry_type: &AppEntryDef,
conductor: &Conductor,
) -> SysValidationResult<EntryDef> {
let ribosome = conductor
.get_ribosome(dna_hash)
.map_err(|_| SysValidationError::DnaMissing(dna_hash.clone()))?;
let zome = ribosome
.get_integrity_zome(&entry_type.zome_index())
.ok_or_else(|| ValidationOutcome::ZomeIndex(entry_type.clone()))?
.into_inner()
.1;
let entry_def = get_entry_def(entry_type.entry_index(), zome, dna_hash, conductor).await?;
match entry_def {
Some(entry_def) => {
if entry_def.visibility == *entry_type.visibility() {
Ok(entry_def)
} else {
Err(ValidationOutcome::EntryVisibility(entry_type.clone()).into())
}
}
None => Err(ValidationOutcome::EntryDefId(entry_type.clone()).into()),
}
}
pub fn check_not_private(entry_def: &EntryDef) -> SysValidationResult<()> {
match entry_def.visibility {
EntryVisibility::Public => Ok(()),
EntryVisibility::Private => Err(ValidationOutcome::PrivateEntry.into()),
}
}
pub async 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(action.clone()).into()),
}
}
pub fn check_entry_size(entry: &Entry) -> SysValidationResult<()> {
match entry {
Entry::App(bytes) => {
let size = std::mem::size_of_val(&bytes.bytes()[..]);
if size < MAX_ENTRY_SIZE {
Ok(())
} else {
Err(ValidationOutcome::EntryTooLarge(size, MAX_ENTRY_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, MAX_TAG_SIZE).into())
}
}
pub fn check_update_reference(
eu: &Update,
original_entry_action: &NewEntryActionRef<'_>,
) -> SysValidationResult<()> {
if eu.entry_type == *original_entry_action.entry_type() {
Ok(())
} else {
Err(ValidationOutcome::UpdateTypeMismatch(
eu.entry_type.clone(),
original_entry_action.entry_type().clone(),
)
.into())
}
}
pub fn validate_chain<'iter, A: 'iter + ChainItem>(
mut actions: impl Iterator<Item = &'iter A>,
persisted_chain_head: &Option<(A::Hash, u32)>,
) -> SysValidationResult<()> {
let mut last_item = match actions.next() {
Some(item) => {
match persisted_chain_head {
Some((prev_hash, prev_seq)) => {
check_prev_action_chain(prev_hash, *prev_seq, item)
.map_err(ValidationOutcome::from)?;
}
None => {
if item.prev_hash().is_some() {
return Err(ValidationOutcome::from(PrevActionError::InvalidRoot).into());
}
}
}
(item.get_hash(), item.seq())
}
None => return Ok(()),
};
for item in actions {
check_prev_action_chain(last_item.0, last_item.1, item).map_err(ValidationOutcome::from)?;
last_item = (item.get_hash(), item.seq());
}
Ok(())
}
fn check_prev_action_chain<A: ChainItem>(
prev_action_hash: &A::Hash,
prev_action_seq: u32,
action: &A,
) -> Result<(), PrevActionError> {
if action.prev_hash().is_none() {
Err(PrevActionError::MissingPrev)
} else if action.prev_hash().map_or(true, |p| p != prev_action_hash) {
Err(PrevActionError::HashMismatch(action.seq()))
} else if action
.seq()
.checked_sub(1)
.map_or(true, |s| prev_action_seq != s)
{
Err(PrevActionError::InvalidSeq(action.seq(), prev_action_seq))
} else {
Ok(())
}
}
pub async fn check_and_hold_register_add_link<F>(
hash: &ActionHash,
cascade: &Cascade,
incoming_dht_ops_sender: Option<IncomingDhtOpSender>,
f: F,
) -> SysValidationResult<()>
where
F: FnOnce(&Record) -> SysValidationResult<()>,
{
let source = check_and_hold(hash, cascade).await?;
f(source.as_ref())?;
if let (Some(incoming_dht_ops_sender), Source::Network(record)) =
(incoming_dht_ops_sender, source)
{
incoming_dht_ops_sender
.send_register_add_link(record)
.await?;
}
Ok(())
}
pub async fn check_and_hold_register_agent_activity<F>(
hash: &ActionHash,
cascade: &Cascade,
incoming_dht_ops_sender: Option<IncomingDhtOpSender>,
f: F,
) -> SysValidationResult<()>
where
F: FnOnce(&Record) -> SysValidationResult<()>,
{
let source = check_and_hold(hash, cascade).await?;
f(source.as_ref())?;
if let (Some(incoming_dht_ops_sender), Source::Network(record)) =
(incoming_dht_ops_sender, source)
{
incoming_dht_ops_sender
.send_register_agent_activity(record)
.await?;
}
Ok(())
}
pub async fn check_and_hold_store_entry<F>(
hash: &ActionHash,
cascade: &Cascade,
incoming_dht_ops_sender: Option<IncomingDhtOpSender>,
f: F,
) -> SysValidationResult<()>
where
F: FnOnce(&Record) -> SysValidationResult<()>,
{
let source = check_and_hold(hash, cascade).await?;
f(source.as_ref())?;
if let (Some(incoming_dht_ops_sender), Source::Network(record)) =
(incoming_dht_ops_sender, source)
{
incoming_dht_ops_sender.send_store_entry(record).await?;
}
Ok(())
}
pub async fn check_and_hold_any_store_entry<F>(
hash: &EntryHash,
cascade: &Cascade,
incoming_dht_ops_sender: Option<IncomingDhtOpSender>,
f: F,
) -> SysValidationResult<()>
where
F: FnOnce(&Record) -> SysValidationResult<()>,
{
let source = check_and_hold(hash, cascade).await?;
f(source.as_ref())?;
if let (Some(incoming_dht_ops_sender), Source::Network(record)) =
(incoming_dht_ops_sender, source)
{
incoming_dht_ops_sender.send_store_entry(record).await?;
}
Ok(())
}
pub async fn check_and_hold_store_record<F>(
hash: &ActionHash,
cascade: &Cascade,
incoming_dht_ops_sender: Option<IncomingDhtOpSender>,
f: F,
) -> SysValidationResult<()>
where
F: FnOnce(&Record) -> SysValidationResult<()>,
{
let source = check_and_hold(hash, cascade).await?;
f(source.as_ref())?;
if let (Some(incoming_dht_ops_sender), Source::Network(record)) =
(incoming_dht_ops_sender, source)
{
incoming_dht_ops_sender.send_store_record(record).await?;
}
Ok(())
}
#[derive(derive_more::Constructor, Clone)]
pub struct IncomingDhtOpSender {
space: Arc<Space>,
sys_validation_trigger: TriggerSender,
}
impl IncomingDhtOpSender {
async fn send_op(
self,
record: Record,
make_op: fn(Record) -> Option<(DhtOpHash, DhtOp)>,
) -> SysValidationResult<()> {
if let Some(op) = make_op(record) {
let ops = vec![op];
incoming_dht_ops_workflow(
self.space.as_ref().clone(),
self.sys_validation_trigger,
ops,
false,
)
.await
.map_err(Box::new)?;
}
Ok(())
}
async fn send_store_record(self, record: Record) -> SysValidationResult<()> {
self.send_op(record, make_store_record).await
}
async fn send_store_entry(self, record: Record) -> SysValidationResult<()> {
let is_public_entry = record.action().entry_type().map_or(false, |et| {
matches!(et.visibility(), EntryVisibility::Public)
});
if is_public_entry {
self.send_op(record, make_store_entry).await?;
}
Ok(())
}
async fn send_register_add_link(self, record: Record) -> SysValidationResult<()> {
self.send_op(record, make_register_add_link).await
}
async fn send_register_agent_activity(self, record: Record) -> SysValidationResult<()> {
self.send_op(record, make_register_agent_activity).await
}
}
enum Source {
Local(Record),
Network(Record),
}
impl AsRef<Record> for Source {
fn as_ref(&self) -> &Record {
match self {
Source::Local(el) | Source::Network(el) => el,
}
}
}
async fn check_and_hold<I: Into<AnyDhtHash> + Clone>(
hash: &I,
cascade: &Cascade,
) -> SysValidationResult<Source> {
let hash: AnyDhtHash = hash.clone().into();
match cascade.retrieve(hash.clone(), Default::default()).await? {
Some((el, CascadeSource::Local)) => Ok(Source::Local(el)),
Some((el, CascadeSource::Network)) => Ok(Source::Network(el.privatized().0)),
None => Err(ValidationOutcome::NotHoldingDep(hash).into()),
}
}
fn make_store_record(record: Record) -> Option<(DhtOpHash, DhtOp)> {
let (shh, record_entry) = record.privatized().0.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content();
let maybe_entry_box = record_entry.into_option().map(Box::new);
let op = DhtOp::StoreRecord(signature, action, maybe_entry_box);
let hash = op.to_hash();
Some((hash, op))
}
fn make_store_entry(record: Record) -> Option<(DhtOpHash, DhtOp)> {
let (shh, record_entry) = record.into_inner();
let (action, signature) = shh.into_inner();
let entry_box = record_entry.into_option()?.into();
let action = action.into_content().try_into().ok()?;
let op = DhtOp::StoreEntry(signature, action, entry_box);
let hash = op.to_hash();
Some((hash, op))
}
fn make_register_add_link(record: Record) -> Option<(DhtOpHash, DhtOp)> {
let (shh, _) = record.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content().try_into().ok()?;
let op = DhtOp::RegisterAddLink(signature, action);
let hash = op.to_hash();
Some((hash, op))
}
fn make_register_agent_activity(record: Record) -> Option<(DhtOpHash, DhtOp)> {
let (shh, _) = record.into_inner();
let (action, signature) = shh.into_inner();
let action = action.into_content();
let op = DhtOp::RegisterAgentActivity(signature, action);
let hash = op.to_hash();
Some((hash, op))
}
#[cfg(test)]
pub mod test {
use super::check_countersigning_preflight_response_signature;
use crate::core::sys_validate::error::SysValidationError;
use crate::core::ValidationOutcome;
use arbitrary::Arbitrary;
use fixt::fixt;
use fixt::Predictable;
use hdk::prelude::AgentPubKeyFixturator;
use holochain_keystore::AgentPubKeyExt;
use holochain_state::test_utils::test_keystore;
use holochain_zome_types::countersigning::PreflightResponse;
use matches::assert_matches;
#[tokio::test(flavor = "multi_thread")]
pub async fn test_check_countersigning_preflight_response_signature() {
let keystore = test_keystore();
let mut u = arbitrary::Unstructured::new(&[0; 1000]);
let mut preflight_response = PreflightResponse::arbitrary(&mut u).unwrap();
assert_matches!(
check_countersigning_preflight_response_signature(&preflight_response).await,
Err(SysValidationError::ValidationOutcome(
ValidationOutcome::PreflightResponseSignature(_)
))
);
let alice = fixt!(AgentPubKey, Predictable);
let bob = fixt!(AgentPubKey, Predictable, 1);
preflight_response
.request_mut()
.signing_agents
.push((alice.clone(), vec![]));
preflight_response
.request_mut()
.signing_agents
.push((bob, vec![]));
*preflight_response.signature_mut() = alice
.sign_raw(
&keystore,
preflight_response.encode_for_signature().unwrap().into(),
)
.await
.unwrap();
assert_eq!(
check_countersigning_preflight_response_signature(&preflight_response)
.await
.unwrap(),
(),
);
}
}