use holo_hash::{
hash_type, ActionHash, AgentPubKey, AnyLinkableHash, DhtOpHash, EntryHash, HasHash,
HashableContent, HashableContentBytes, HoloHashed,
};
use holochain_serialized_bytes::prelude::*;
use holochain_zome_types::op::ChainOpType;
use holochain_zome_types::prelude::*;
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 enum DhtOp {
ChainOp(Box<ChainOp>),
WarrantOp(Box<crate::warrant::WarrantOp>),
}
impl From<ChainOp> for DhtOp {
fn from(op: ChainOp) -> Self {
DhtOp::ChainOp(Box::new(op))
}
}
impl From<crate::warrant::WarrantOp> for DhtOp {
fn from(op: crate::warrant::WarrantOp) -> Self {
DhtOp::WarrantOp(Box::new(op))
}
}
impl From<SignedWarrant> for DhtOp {
fn from(op: SignedWarrant) -> Self {
DhtOp::WarrantOp(Box::new(crate::warrant::WarrantOp::from(op)))
}
}
#[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 from_type(
op_type: holochain_zome_types::op::ChainOpType,
signed_action: SignedAction,
op_entry: OpEntry,
) -> Self {
use holochain_zome_types::op::ChainOpType;
match op_type {
ChainOpType::CreateRecord => ChainOp::CreateRecord(signed_action, op_entry),
ChainOpType::CreateEntry => ChainOp::CreateEntry(signed_action, op_entry),
ChainOpType::AgentActivity => ChainOp::AgentActivity(signed_action),
ChainOpType::UpdateEntry => ChainOp::UpdateEntry(signed_action, op_entry),
ChainOpType::UpdateRecord => ChainOp::UpdateRecord(signed_action, op_entry),
ChainOpType::DeleteEntry => ChainOp::DeleteEntry(signed_action),
ChainOpType::DeleteRecord => ChainOp::DeleteRecord(signed_action),
ChainOpType::CreateLink => ChainOp::CreateLink(signed_action),
ChainOpType::DeleteLink => ChainOp::DeleteLink(signed_action),
}
}
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::CreateRecord,
ChainOp::CreateEntry(..) => ChainOpType::CreateEntry,
ChainOp::AgentActivity(..) => ChainOpType::AgentActivity,
ChainOp::UpdateEntry(..) => ChainOpType::UpdateEntry,
ChainOp::UpdateRecord(..) => ChainOpType::UpdateRecord,
ChainOp::DeleteEntry(..) => ChainOpType::DeleteEntry,
ChainOp::DeleteRecord(..) => ChainOpType::DeleteRecord,
ChainOp::CreateLink(..) => ChainOpType::CreateLink,
ChainOp::DeleteLink(..) => ChainOpType::DeleteLink,
}
}
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")
}
pub fn enzymatic_countersigning_enzyme(&self) -> Option<&AgentPubKey> {
let OpEntry::Present(entry) = self.op_entry()? else {
return None;
};
let Entry::CounterSign(session_data, _) = entry else {
return None;
};
if session_data.preflight_request().enzymatic {
session_data
.preflight_request()
.signing_agents
.first()
.map(|(pubkey, _)| pubkey)
} else {
None
}
}
}
impl DhtOp {
pub fn to_hash(&self) -> DhtOpHash {
match self {
DhtOp::ChainOp(op) => op.to_hash(),
DhtOp::WarrantOp(op) => DhtOpHash::with_data_sync(op.as_ref()),
}
}
pub fn dht_basis(&self) -> AnyLinkableHash {
match self {
DhtOp::ChainOp(op) => op.dht_basis(),
DhtOp::WarrantOp(op) => op.data().warrantee.clone().into(),
}
}
}
pub type ChainOpHashed = HoloHashed<ChainOp>;
pub type DhtOpHashed = HoloHashed<DhtOp>;
impl HashableContent for ChainOp {
type HashType = hash_type::DhtOp;
fn hash_type(&self) -> Self::HashType {
hash_type::DhtOp
}
fn hashable_content(&self) -> HashableContentBytes {
HashableContentBytes::Prehashed39(self.to_hash().get_raw_39().to_vec())
}
}
impl HashableContent for DhtOp {
type HashType = hash_type::DhtOp;
fn hash_type(&self) -> Self::HashType {
hash_type::DhtOp
}
fn hashable_content(&self) -> HashableContentBytes {
HashableContentBytes::Prehashed39(self.to_hash().get_raw_39().to_vec())
}
}
impl ChainOpUniqueForm<'_> {
pub fn op_hash(op_type: ChainOpType, action: &Action) -> DhtOpHash {
let form = match op_type {
ChainOpType::CreateRecord => ChainOpUniqueForm::CreateRecord(action),
ChainOpType::CreateEntry => ChainOpUniqueForm::CreateEntry(action),
ChainOpType::AgentActivity => ChainOpUniqueForm::AgentActivity(action),
ChainOpType::UpdateEntry => ChainOpUniqueForm::UpdateEntry(action),
ChainOpType::UpdateRecord => ChainOpUniqueForm::UpdateRecord(action),
ChainOpType::DeleteRecord => ChainOpUniqueForm::DeleteRecord(action),
ChainOpType::DeleteEntry => ChainOpUniqueForm::DeleteEntry(action),
ChainOpType::CreateLink => ChainOpUniqueForm::CreateLink(action),
ChainOpType::DeleteLink => 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![CreateRecord, AgentActivity],
ActionData::CreateLink(_) => vec![CreateRecord, AgentActivity, CreateLink],
ActionData::DeleteLink(_) => vec![CreateRecord, AgentActivity, DeleteLink],
ActionData::Create(_) => vec![CreateRecord, AgentActivity, CreateEntry],
ActionData::Update(_) => vec![
CreateRecord,
AgentActivity,
CreateEntry,
UpdateEntry,
UpdateRecord,
],
ActionData::Delete(_) => vec![CreateRecord, AgentActivity, DeleteRecord, DeleteEntry],
}
}
fn op_basis(
op_type: ChainOpType,
action_hash: &ActionHash,
action: &Action,
) -> Option<AnyLinkableHash> {
use ChainOpType::*;
Some(match (op_type, &action.data) {
(CreateRecord, _) => action_hash.clone().into(),
(AgentActivity, _) => action.header.author.clone().into(),
(CreateEntry, ActionData::Create(d)) => d.entry_hash.clone().into(),
(CreateEntry, ActionData::Update(d)) => d.entry_hash.clone().into(),
(UpdateEntry, ActionData::Update(d)) => d.original_entry_address.clone().into(),
(UpdateRecord, ActionData::Update(d)) => d.original_action_address.clone().into(),
(DeleteRecord, ActionData::Delete(d)) => d.deletes_address.clone().into(),
(DeleteEntry, ActionData::Delete(d)) => d.deletes_entry_address.clone().into(),
(CreateLink, ActionData::CreateLink(d)) => d.base_address.clone(),
(DeleteLink, 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::CreateEntry && 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::CreateRecord
| ChainOpType::CreateEntry
| ChainOpType::UpdateEntry
| ChainOpType::UpdateRecord
)
}
#[derive(Clone, Debug, PartialEq, Eq)]
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 from_signed_action(
action: SignedActionHashed,
op_type: ChainOpType,
entry: Option<HoloHashed<Entry>>,
) -> Option<Self> {
let op_hash = ChainOpUniqueForm::op_hash(op_type, action.action());
let basis_hash = op_basis(op_type, action.as_hash(), action.action())?;
Some(Self {
op_hash,
action,
entry,
op_type,
storage_center_loc: basis_hash.get_loc(),
basis_hash,
})
}
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::CreateRecord,
),
(
ChainOp::CreateEntry(sa.clone(), OpEntry::ActionOnly),
ChainOpType::CreateEntry,
),
(
ChainOp::AgentActivity(sa.clone()),
ChainOpType::AgentActivity,
),
(
ChainOp::UpdateEntry(sa.clone(), OpEntry::ActionOnly),
ChainOpType::UpdateEntry,
),
(
ChainOp::UpdateRecord(sa.clone(), OpEntry::ActionOnly),
ChainOpType::UpdateRecord,
),
(ChainOp::DeleteRecord(sa.clone()), ChainOpType::DeleteRecord),
(ChainOp::DeleteEntry(sa.clone()), ChainOpType::DeleteEntry),
(ChainOp::CreateLink(sa.clone()), ChainOpType::CreateLink),
(ChainOp::DeleteLink(sa.clone()), ChainOpType::DeleteLink),
];
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::CreateRecord,
ChainOpType::AgentActivity,
ChainOpType::CreateEntry,
]
);
}
#[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::CreateRecord, &action_hash, &action).unwrap();
assert_eq!(record_basis, AnyLinkableHash::from(action_hash.clone()));
let entry_basis = op_basis(ChainOpType::CreateEntry, &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::prelude::{AppEntryDef, EntryType, EntryVisibility};
use holochain_zome_types::record::{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::CreateRecord,
ChainOpType::AgentActivity,
ChainOpType::CreateEntry,
]
);
let by_type = |t| ops.iter().find(|o| o.op_type == t).unwrap();
assert!(by_type(ChainOpType::CreateRecord).entry.is_some());
assert!(by_type(ChainOpType::CreateEntry).entry.is_some());
assert!(by_type(ChainOpType::AgentActivity).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::CreateRecord, ChainOpType::AgentActivity]
);
let store_record = ops
.iter()
.find(|o| o.op_type == ChainOpType::CreateRecord)
.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::CreateRecord)
.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::CreateRecord,
ChainOpType::AgentActivity,
ChainOpType::CreateEntry,
ChainOpType::UpdateEntry,
ChainOpType::UpdateRecord,
]
);
let by_type = |t| ops.iter().find(|o| o.op_type == t).unwrap();
assert!(by_type(ChainOpType::UpdateEntry).entry.is_some());
assert!(by_type(ChainOpType::UpdateRecord).entry.is_some());
assert!(by_type(ChainOpType::AgentActivity).entry.is_none());
assert_eq!(
by_type(ChainOpType::UpdateEntry).basis_hash,
AnyLinkableHash::from(EntryHash::from_raw_36(vec![7u8; 36]))
);
assert_eq!(
by_type(ChainOpType::UpdateRecord).basis_hash,
AnyLinkableHash::from(ActionHash::from_raw_36(vec![6u8; 36]))
);
}
}