pub use holochain_zome_types::dht_v2::*;
use holo_hash::{
hash_type, ActionHash, AnyLinkableHash, DhtOpHash, EntryHash, HasHash, HashableContent,
HashableContentBytes, HoloHashed,
};
use holochain_serialized_bytes::prelude::*;
use holochain_zome_types::op::ChainOpType;
use holochain_zome_types::Entry;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub enum OpEntry {
Present(Entry),
Hidden,
ActionOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub enum ChainOp {
CreateRecord(SignedAction, OpEntry),
CreateEntry(SignedAction, OpEntry),
AgentActivity(SignedAction),
UpdateEntry(SignedAction, OpEntry),
UpdateRecord(SignedAction, OpEntry),
DeleteEntry(SignedAction),
DeleteRecord(SignedAction),
CreateLink(SignedAction),
DeleteLink(SignedAction),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct WarrantOp(pub SignedWarrant);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub enum DhtOp {
ChainOp(Box<ChainOp>),
WarrantOp(Box<WarrantOp>),
}
#[allow(missing_docs)]
#[derive(serde::Serialize, Debug)]
pub enum ChainOpUniqueForm<'a> {
CreateRecord(&'a Action),
CreateEntry(&'a Action),
AgentActivity(&'a Action),
UpdateEntry(&'a Action),
UpdateRecord(&'a Action),
DeleteEntry(&'a Action),
DeleteRecord(&'a Action),
CreateLink(&'a Action),
DeleteLink(&'a Action),
}
impl HashableContent for ChainOpUniqueForm<'_> {
type HashType = hash_type::DhtOp;
fn hash_type(&self) -> Self::HashType {
hash_type::DhtOp
}
fn hashable_content(&self) -> HashableContentBytes {
HashableContentBytes::Content(
UnsafeBytes::from(
holochain_serialized_bytes::encode(self)
.expect("Could not serialize HashableContent"),
)
.into(),
)
}
}
impl ChainOp {
pub fn to_hash(&self) -> DhtOpHash {
DhtOpHash::with_data_sync(&self.to_unique_form())
}
fn to_unique_form(&self) -> ChainOpUniqueForm<'_> {
match self {
ChainOp::CreateRecord(sa, _) => ChainOpUniqueForm::CreateRecord(sa.data()),
ChainOp::CreateEntry(sa, _) => ChainOpUniqueForm::CreateEntry(sa.data()),
ChainOp::AgentActivity(sa) => ChainOpUniqueForm::AgentActivity(sa.data()),
ChainOp::UpdateEntry(sa, _) => ChainOpUniqueForm::UpdateEntry(sa.data()),
ChainOp::UpdateRecord(sa, _) => ChainOpUniqueForm::UpdateRecord(sa.data()),
ChainOp::DeleteEntry(sa) => ChainOpUniqueForm::DeleteEntry(sa.data()),
ChainOp::DeleteRecord(sa) => ChainOpUniqueForm::DeleteRecord(sa.data()),
ChainOp::CreateLink(sa) => ChainOpUniqueForm::CreateLink(sa.data()),
ChainOp::DeleteLink(sa) => ChainOpUniqueForm::DeleteLink(sa.data()),
}
}
pub fn op_type(&self) -> ChainOpType {
match self {
ChainOp::CreateRecord(..) => ChainOpType::StoreRecord,
ChainOp::CreateEntry(..) => ChainOpType::StoreEntry,
ChainOp::AgentActivity(..) => ChainOpType::RegisterAgentActivity,
ChainOp::UpdateEntry(..) => ChainOpType::RegisterUpdatedContent,
ChainOp::UpdateRecord(..) => ChainOpType::RegisterUpdatedRecord,
ChainOp::DeleteEntry(..) => ChainOpType::RegisterDeletedEntryAction,
ChainOp::DeleteRecord(..) => ChainOpType::RegisterDeletedBy,
ChainOp::CreateLink(..) => ChainOpType::RegisterAddLink,
ChainOp::DeleteLink(..) => ChainOpType::RegisterRemoveLink,
}
}
pub fn signed_action(&self) -> &SignedAction {
match self {
ChainOp::CreateRecord(sa, _)
| ChainOp::CreateEntry(sa, _)
| ChainOp::UpdateEntry(sa, _)
| ChainOp::UpdateRecord(sa, _) => sa,
ChainOp::AgentActivity(sa)
| ChainOp::DeleteEntry(sa)
| ChainOp::DeleteRecord(sa)
| ChainOp::CreateLink(sa)
| ChainOp::DeleteLink(sa) => sa,
}
}
pub fn op_entry(&self) -> Option<&OpEntry> {
match self {
ChainOp::CreateRecord(_, e)
| ChainOp::CreateEntry(_, e)
| ChainOp::UpdateEntry(_, e)
| ChainOp::UpdateRecord(_, e) => Some(e),
ChainOp::AgentActivity(_)
| ChainOp::DeleteEntry(_)
| ChainOp::DeleteRecord(_)
| ChainOp::CreateLink(_)
| ChainOp::DeleteLink(_) => None,
}
}
pub fn dht_basis(&self) -> AnyLinkableHash {
let action = self.signed_action().data();
let action_hash = ActionHash::with_data_sync(action);
op_basis(self.op_type(), &action_hash, action)
.expect("op_basis is total over an op paired with its own action")
}
}
impl DhtOp {
pub fn to_hash(&self) -> DhtOpHash {
match self {
DhtOp::ChainOp(op) => op.to_hash(),
DhtOp::WarrantOp(op) => {
DhtOpHash::with_data_sync(&crate::warrant::WarrantOp::from(op.0.clone()))
}
}
}
pub fn dht_basis(&self) -> AnyLinkableHash {
match self {
DhtOp::ChainOp(op) => op.dht_basis(),
DhtOp::WarrantOp(op) => op.0.data().warrantee.clone().into(),
}
}
}
pub fn to_legacy_dht_op(op: &DhtOp) -> crate::dht_op::DhtOpResult<crate::dht_op::DhtOp> {
use crate::dht_op::{ChainOp as LegacyChainOp, DhtOp as LegacyDhtOp};
match op {
DhtOp::ChainOp(chain_op) => {
let signed = chain_op.signed_action();
let hashed = HoloHashed::from_content_sync(signed.data().clone());
let v2_sah = holochain_zome_types::record::SignedHashed::with_presigned(
hashed,
signed.signature().clone(),
);
let legacy_sah = to_legacy_signed_action(&v2_sah);
let entry = chain_op.op_entry().and_then(|e| match e {
OpEntry::Present(entry) => Some(entry.clone()),
OpEntry::Hidden | OpEntry::ActionOnly => None,
});
let legacy = LegacyChainOp::from_type(chain_op.op_type(), legacy_sah.into(), entry)?;
Ok(LegacyDhtOp::ChainOp(Box::new(legacy)))
}
DhtOp::WarrantOp(w) => Ok(LegacyDhtOp::WarrantOp(Box::new(
crate::warrant::WarrantOp::from(w.0.clone()),
))),
}
}
impl ChainOpUniqueForm<'_> {
pub fn op_hash(op_type: ChainOpType, action: &Action) -> DhtOpHash {
let form = match op_type {
ChainOpType::StoreRecord => ChainOpUniqueForm::CreateRecord(action),
ChainOpType::StoreEntry => ChainOpUniqueForm::CreateEntry(action),
ChainOpType::RegisterAgentActivity => ChainOpUniqueForm::AgentActivity(action),
ChainOpType::RegisterUpdatedContent => ChainOpUniqueForm::UpdateEntry(action),
ChainOpType::RegisterUpdatedRecord => ChainOpUniqueForm::UpdateRecord(action),
ChainOpType::RegisterDeletedBy => ChainOpUniqueForm::DeleteRecord(action),
ChainOpType::RegisterDeletedEntryAction => ChainOpUniqueForm::DeleteEntry(action),
ChainOpType::RegisterAddLink => ChainOpUniqueForm::CreateLink(action),
ChainOpType::RegisterRemoveLink => ChainOpUniqueForm::DeleteLink(action),
};
DhtOpHash::with_data_sync(&form)
}
}
pub fn action_to_op_types(action: &Action) -> Vec<ChainOpType> {
use ChainOpType::*;
match &action.data {
ActionData::Dna(_)
| ActionData::OpenChain(_)
| ActionData::CloseChain(_)
| ActionData::AgentValidationPkg(_)
| ActionData::InitZomesComplete(_) => vec![StoreRecord, RegisterAgentActivity],
ActionData::CreateLink(_) => vec![StoreRecord, RegisterAgentActivity, RegisterAddLink],
ActionData::DeleteLink(_) => vec![StoreRecord, RegisterAgentActivity, RegisterRemoveLink],
ActionData::Create(_) => vec![StoreRecord, RegisterAgentActivity, StoreEntry],
ActionData::Update(_) => vec![
StoreRecord,
RegisterAgentActivity,
StoreEntry,
RegisterUpdatedContent,
RegisterUpdatedRecord,
],
ActionData::Delete(_) => vec![
StoreRecord,
RegisterAgentActivity,
RegisterDeletedBy,
RegisterDeletedEntryAction,
],
}
}
fn op_basis(
op_type: ChainOpType,
action_hash: &ActionHash,
action: &Action,
) -> Option<AnyLinkableHash> {
use ChainOpType::*;
Some(match (op_type, &action.data) {
(StoreRecord, _) => action_hash.clone().into(),
(RegisterAgentActivity, _) => action.header.author.clone().into(),
(StoreEntry, ActionData::Create(d)) => d.entry_hash.clone().into(),
(StoreEntry, ActionData::Update(d)) => d.entry_hash.clone().into(),
(RegisterUpdatedContent, ActionData::Update(d)) => d.original_entry_address.clone().into(),
(RegisterUpdatedRecord, ActionData::Update(d)) => d.original_action_address.clone().into(),
(RegisterDeletedBy, ActionData::Delete(d)) => d.deletes_address.clone().into(),
(RegisterDeletedEntryAction, ActionData::Delete(d)) => {
d.deletes_entry_address.clone().into()
}
(RegisterAddLink, ActionData::CreateLink(d)) => d.base_address.clone(),
(RegisterRemoveLink, ActionData::DeleteLink(d)) => d.base_address.clone(),
_ => return None,
})
}
pub fn produce_ops_from_record(record: &Record) -> Vec<HashedChainOp> {
let action = record.action();
let action_hash = record.action_address();
let entry = record.entry.as_option();
let mut ops = Vec::new();
for op_type in action_to_op_types(action) {
if op_type == ChainOpType::StoreEntry && entry.is_none() {
continue;
}
let Some(basis_hash) = op_basis(op_type, action_hash, action) else {
continue;
};
let op_entry = if op_carries_entry(op_type) {
entry.map(|e| HoloHashed::from_content_sync(e.clone()))
} else {
None
};
ops.push(HashedChainOp {
op_hash: ChainOpUniqueForm::op_hash(op_type, action),
action: record.signed_action.clone(),
entry: op_entry,
op_type,
storage_center_loc: basis_hash.get_loc(),
basis_hash,
});
}
ops
}
fn op_carries_entry(op_type: ChainOpType) -> bool {
matches!(
op_type,
ChainOpType::StoreRecord
| ChainOpType::StoreEntry
| ChainOpType::RegisterUpdatedContent
| ChainOpType::RegisterUpdatedRecord
)
}
#[derive(Clone, Debug)]
pub struct HashedChainOp {
pub op_hash: DhtOpHash,
pub action: SignedActionHashed,
pub entry: Option<HoloHashed<Entry>>,
pub op_type: ChainOpType,
pub basis_hash: AnyLinkableHash,
pub storage_center_loc: u32,
}
impl HashedChainOp {
pub fn action_hash(&self) -> &ActionHash {
self.action.as_hash()
}
pub fn entry_hash(&self) -> Option<&EntryHash> {
self.entry.as_ref().map(|e| e.as_hash())
}
}
#[cfg(test)]
mod op_hash_tests {
use super::*;
use holo_hash::{ActionHash, AgentPubKey, EntryHash};
use holochain_timestamp::Timestamp;
use holochain_zome_types::prelude::{AppEntryDef, EntryType, EntryVisibility};
use holochain_zome_types::signature::Signature;
use holochain_zome_types::Entry;
fn create_action() -> Action {
Action {
header: ActionHeader {
author: AgentPubKey::from_raw_36(vec![1u8; 36]),
timestamp: Timestamp::from_micros(1_000),
action_seq: 4,
prev_action: Some(ActionHash::from_raw_36(vec![2u8; 36])),
},
data: ActionData::Create(CreateData {
entry_type: EntryType::App(AppEntryDef::new(
0.into(),
0.into(),
EntryVisibility::Public,
)),
entry_hash: EntryHash::from_raw_36(vec![3u8; 36]),
}),
}
}
fn signed(action: Action, sig: u8) -> SignedAction {
SignedAction::new(action, Signature::from([sig; 64]))
}
#[test]
fn op_hash_is_deterministic() {
let a = ChainOp::CreateRecord(signed(create_action(), 7), OpEntry::ActionOnly);
let b = ChainOp::CreateRecord(signed(create_action(), 7), OpEntry::ActionOnly);
assert_eq!(a.to_hash(), b.to_hash());
}
#[test]
fn op_hash_differs_by_op_type() {
let sa = signed(create_action(), 7);
let record = ChainOp::CreateRecord(sa.clone(), OpEntry::ActionOnly);
let entry = ChainOp::CreateEntry(sa, OpEntry::ActionOnly);
assert_ne!(record.to_hash(), entry.to_hash());
}
#[test]
fn op_hash_ignores_signature_and_entry() {
let action = create_action();
let with_sig_7 = ChainOp::CreateRecord(signed(action.clone(), 7), OpEntry::ActionOnly);
let with_sig_9_and_entry = ChainOp::CreateRecord(
signed(action, 9),
OpEntry::Present(Entry::Agent(AgentPubKey::from_raw_36(vec![5u8; 36]))),
);
assert_eq!(with_sig_7.to_hash(), with_sig_9_and_entry.to_hash());
}
#[test]
fn op_hash_is_content_derived() {
let base = ChainOp::CreateRecord(signed(create_action(), 7), OpEntry::ActionOnly);
let mut changed_action = create_action();
changed_action.header.action_seq = 99;
let changed = ChainOp::CreateRecord(signed(changed_action, 7), OpEntry::ActionOnly);
assert_ne!(base.to_hash(), changed.to_hash());
}
#[test]
fn op_hash_entry_point_matches_chain_op_to_hash() {
let sa = signed(create_action(), 7);
let cases = [
(
ChainOp::CreateRecord(sa.clone(), OpEntry::ActionOnly),
ChainOpType::StoreRecord,
),
(
ChainOp::CreateEntry(sa.clone(), OpEntry::ActionOnly),
ChainOpType::StoreEntry,
),
(
ChainOp::AgentActivity(sa.clone()),
ChainOpType::RegisterAgentActivity,
),
(
ChainOp::UpdateEntry(sa.clone(), OpEntry::ActionOnly),
ChainOpType::RegisterUpdatedContent,
),
(
ChainOp::UpdateRecord(sa.clone(), OpEntry::ActionOnly),
ChainOpType::RegisterUpdatedRecord,
),
(
ChainOp::DeleteRecord(sa.clone()),
ChainOpType::RegisterDeletedBy,
),
(
ChainOp::DeleteEntry(sa.clone()),
ChainOpType::RegisterDeletedEntryAction,
),
(
ChainOp::CreateLink(sa.clone()),
ChainOpType::RegisterAddLink,
),
(
ChainOp::DeleteLink(sa.clone()),
ChainOpType::RegisterRemoveLink,
),
];
for (op, op_type) in cases {
assert_eq!(op.to_hash(), ChainOpUniqueForm::op_hash(op_type, sa.data()));
}
}
#[test]
fn dht_op_hash_and_basis_delegate_to_chain_op() {
let sa = signed(create_action(), 7);
let chain_op = ChainOp::CreateRecord(sa.clone(), OpEntry::ActionOnly);
let dht_op = DhtOp::ChainOp(Box::new(chain_op.clone()));
assert_eq!(dht_op.to_hash(), chain_op.to_hash());
let action_hash = ActionHash::with_data_sync(sa.data());
assert_eq!(dht_op.dht_basis(), AnyLinkableHash::from(action_hash));
assert_eq!(dht_op.dht_basis(), chain_op.dht_basis());
}
#[test]
fn action_to_op_types_create_produces_record_activity_entry() {
let action = create_action();
assert_eq!(
action_to_op_types(&action),
vec![
ChainOpType::StoreRecord,
ChainOpType::RegisterAgentActivity,
ChainOpType::StoreEntry,
]
);
}
#[test]
fn op_basis_uses_action_hash_for_store_record_and_entry_hash_for_store_entry() {
use holo_hash::AnyLinkableHash;
let action = create_action();
let action_hash = ActionHash::from_raw_36(vec![8u8; 36]);
let record_basis = op_basis(ChainOpType::StoreRecord, &action_hash, &action).unwrap();
assert_eq!(record_basis, AnyLinkableHash::from(action_hash.clone()));
let entry_basis = op_basis(ChainOpType::StoreEntry, &action_hash, &action).unwrap();
assert_eq!(
entry_basis,
AnyLinkableHash::from(EntryHash::from_raw_36(vec![3u8; 36]))
);
}
}
#[cfg(test)]
mod produce_ops_tests {
use super::*;
use holo_hash::{ActionHash, AgentPubKey, EntryHash, HoloHashed};
use holochain_timestamp::Timestamp;
use holochain_zome_types::dht_v2::Record;
use holochain_zome_types::prelude::{AppEntryDef, EntryType, EntryVisibility};
use holochain_zome_types::record::{RecordEntry, SignedHashed};
use holochain_zome_types::signature::Signature;
use holochain_zome_types::Entry;
fn create_action() -> Action {
Action {
header: ActionHeader {
author: AgentPubKey::from_raw_36(vec![1u8; 36]),
timestamp: Timestamp::from_micros(1_000),
action_seq: 4,
prev_action: Some(ActionHash::from_raw_36(vec![2u8; 36])),
},
data: ActionData::Create(CreateData {
entry_type: EntryType::App(AppEntryDef::new(
0.into(),
0.into(),
EntryVisibility::Public,
)),
entry_hash: EntryHash::from_raw_36(vec![3u8; 36]),
}),
}
}
fn update_action() -> Action {
Action {
header: ActionHeader {
author: AgentPubKey::from_raw_36(vec![1u8; 36]),
timestamp: Timestamp::from_micros(2_000),
action_seq: 5,
prev_action: Some(ActionHash::from_raw_36(vec![2u8; 36])),
},
data: ActionData::Update(UpdateData {
original_action_address: ActionHash::from_raw_36(vec![6u8; 36]),
original_entry_address: EntryHash::from_raw_36(vec![7u8; 36]),
entry_type: EntryType::App(AppEntryDef::new(
0.into(),
0.into(),
EntryVisibility::Public,
)),
entry_hash: EntryHash::from_raw_36(vec![3u8; 36]),
}),
}
}
fn record(action: Action, entry: RecordEntry<Entry>) -> Record {
let hashed = HoloHashed::with_pre_hashed(action, ActionHash::from_raw_36(vec![9u8; 36]));
Record::new(
SignedHashed::with_presigned(hashed, Signature([7u8; 64])),
entry,
)
}
fn op_types(ops: &[HashedChainOp]) -> Vec<ChainOpType> {
ops.iter().map(|o| o.op_type).collect()
}
#[test]
fn create_with_public_entry_produces_record_activity_entry() {
let entry = Entry::Agent(AgentPubKey::from_raw_36(vec![5u8; 36]));
let ops = produce_ops_from_record(&record(create_action(), RecordEntry::Present(entry)));
assert_eq!(
op_types(&ops),
vec![
ChainOpType::StoreRecord,
ChainOpType::RegisterAgentActivity,
ChainOpType::StoreEntry,
]
);
let by_type = |t| ops.iter().find(|o| o.op_type == t).unwrap();
assert!(by_type(ChainOpType::StoreRecord).entry.is_some());
assert!(by_type(ChainOpType::StoreEntry).entry.is_some());
assert!(by_type(ChainOpType::RegisterAgentActivity).entry.is_none());
}
#[test]
fn create_with_hidden_entry_skips_store_entry_and_omits_payload() {
let ops = produce_ops_from_record(&record(create_action(), RecordEntry::Hidden));
assert_eq!(
op_types(&ops),
vec![ChainOpType::StoreRecord, ChainOpType::RegisterAgentActivity]
);
let store_record = ops
.iter()
.find(|o| o.op_type == ChainOpType::StoreRecord)
.unwrap();
assert!(store_record.entry.is_none());
}
#[test]
fn store_record_basis_is_the_action_hash() {
use holo_hash::AnyLinkableHash;
let r = record(create_action(), RecordEntry::Hidden);
let action_hash = r.action_address().clone();
let ops = produce_ops_from_record(&r);
let store_record = ops
.iter()
.find(|o| o.op_type == ChainOpType::StoreRecord)
.unwrap();
assert_eq!(store_record.basis_hash, AnyLinkableHash::from(action_hash));
}
#[test]
fn update_with_public_entry_produces_full_op_set_with_update_payloads() {
use holo_hash::AnyLinkableHash;
let entry = Entry::Agent(AgentPubKey::from_raw_36(vec![5u8; 36]));
let ops = produce_ops_from_record(&record(update_action(), RecordEntry::Present(entry)));
assert_eq!(
op_types(&ops),
vec![
ChainOpType::StoreRecord,
ChainOpType::RegisterAgentActivity,
ChainOpType::StoreEntry,
ChainOpType::RegisterUpdatedContent,
ChainOpType::RegisterUpdatedRecord,
]
);
let by_type = |t| ops.iter().find(|o| o.op_type == t).unwrap();
assert!(by_type(ChainOpType::RegisterUpdatedContent).entry.is_some());
assert!(by_type(ChainOpType::RegisterUpdatedRecord).entry.is_some());
assert!(by_type(ChainOpType::RegisterAgentActivity).entry.is_none());
assert_eq!(
by_type(ChainOpType::RegisterUpdatedContent).basis_hash,
AnyLinkableHash::from(EntryHash::from_raw_36(vec![7u8; 36]))
);
assert_eq!(
by_type(ChainOpType::RegisterUpdatedRecord).basis_hash,
AnyLinkableHash::from(ActionHash::from_raw_36(vec![6u8; 36]))
);
}
}