use crate::chain_lock::ChainLock;
use crate::prelude::*;
use crate::scratch::ScratchError;
use crate::scratch::SyncScratchError;
use async_recursion::async_recursion;
pub use error::*;
use holo_hash::ActionHash;
use holo_hash::AgentPubKey;
use holo_hash::DnaHash;
use holo_hash::EntryHash;
use holo_hash::HasHash;
use holo_hash::HoloHashed;
use holochain_data::kind::Dht;
use holochain_data::{DbRead, DbWrite};
use holochain_keystore::{AgentPubKeyExt, MetaLairClient, SignedActionHashedExt};
use holochain_state_types::{SourceChainCursor, SourceChainDump};
use holochain_types::op::{
produce_ops_from_record, ChainOp, DhtOp, DhtOpHashed, HashedChainOp, OpEntry,
};
use holochain_types::warrant::WarrantOp;
use holochain_zome_types::prelude::{
build_action, from_countersigning_data, Action, ActionData, ActionHeader,
AgentValidationPkgData, CreateData, DeleteData, DnaData, Record, RecordValidity, SignedAction,
SignedActionHashed,
};
use kitsune2_api::DhtArc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
mod error;
#[derive(Clone)]
pub struct SourceChain<Db = DbWrite<Dht>> {
scratch: SyncScratch,
pub(crate) dht_store: DhtStore<Db>,
keystore: MetaLairClient,
author: Arc<AgentPubKey>,
cell_id: Arc<CellId>,
head_info: Option<HeadInfo>,
public_only: bool,
zomes_initialized: Arc<AtomicBool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HeadInfo {
pub action: ActionHash,
pub seq: u32,
pub timestamp: Timestamp,
}
impl HeadInfo {
pub fn into_tuple(self) -> (ActionHash, u32, Timestamp) {
(self.action, self.seq, self.timestamp)
}
}
pub type SourceChainRead = SourceChain<DbRead<Dht>>;
impl SourceChain<DbWrite<Dht>> {
#[cfg_attr(feature = "instrument", tracing::instrument(skip_all))]
pub async fn unlock_chain(&self) -> SourceChainResult<()> {
self.dht_store
.release_chain_lock(self.author.as_ref())
.await?;
Ok(())
}
#[cfg_attr(feature = "instrument", tracing::instrument(skip_all))]
pub async fn accept_countersigning_preflight_request(
&self,
preflight_request: PreflightRequest,
agent_index: u8,
) -> SourceChainResult<CounterSigningAgentState> {
let hashed_preflight_request =
blake2b_256(&holochain_serialized_bytes::encode(&preflight_request)?);
let author = self.author.clone();
assert_eq!(
*author,
preflight_request.signing_agents[agent_index as usize].0
);
if self
.dht_store
.as_read()
.get_chain_lock(author.as_ref().clone())
.await?
.is_some()
{
return Err(SourceChainError::ChainLocked);
}
let HeadInfo {
action: persisted_head,
seq: persisted_seq,
..
} = self
.dht_store
.as_read()
.chain_head_for_author(author.as_ref())
.await?
.ok_or(SourceChainError::ChainEmpty)?;
let countersigning_agent_state =
CounterSigningAgentState::new(agent_index, persisted_head, persisted_seq);
let acquired = self
.dht_store
.acquire_chain_lock(
author.as_ref(),
&hashed_preflight_request,
*preflight_request.session_times.end(),
Timestamp::now(),
)
.await?;
if !acquired {
return Err(SourceChainError::ChainLocked);
}
Ok(countersigning_agent_state)
}
pub async fn put_with_action(
&self,
action: Action,
maybe_entry: Option<Entry>,
chain_top_ordering: ChainTopOrdering,
) -> SourceChainResult<ActionHash> {
let entry_visibility = action.entry_visibility().copied();
let action_hashed = HoloHashed::<Action>::from_content_sync(action);
let hash = action_hashed.as_hash().clone();
let signed_action = SignedActionHashed::sign(&self.keystore, action_hashed).await?;
let record_entry =
holochain_zome_types::prelude::RecordEntry::new(entry_visibility.as_ref(), maybe_entry);
let record = Record::new(signed_action, record_entry);
self.scratch
.apply(|scratch| insert_record_scratch(scratch, record, chain_top_ordering))?;
Ok(hash)
}
pub async fn put_countersigned(
&self,
entry: Entry,
chain_top_ordering: ChainTopOrdering,
) -> SourceChainResult<ActionHash> {
let entry_hash = EntryHash::with_data_sync(&entry);
if let Entry::CounterSign(ref session_data, _) = entry {
self.put_with_action(
from_countersigning_data(entry_hash, session_data, (*self.author).clone())?,
Some(entry),
chain_top_ordering,
)
.await
} else {
unreachable!("Put countersigned called with the wrong entry type");
}
}
pub async fn put(
&self,
data: ActionData,
maybe_entry: Option<Entry>,
chain_top_ordering: ChainTopOrdering,
) -> SourceChainResult<ActionHash> {
let HeadInfo {
action: prev_action,
seq: chain_head_seq,
timestamp: chain_head_timestamp,
} = self.chain_head_nonempty()?;
let action_seq = chain_head_seq + 1;
let header = ActionHeader {
author: (*self.author).clone(),
timestamp: std::cmp::max(
Timestamp::now(),
(chain_head_timestamp + std::time::Duration::from_micros(1))?,
),
action_seq,
prev_action: Some(prev_action),
};
self.put_with_action(build_action(header, data), maybe_entry, chain_top_ordering)
.await
}
#[async_recursion]
#[cfg_attr(feature = "instrument", tracing::instrument(skip(self)))]
#[allow(clippy::only_used_in_recursion)]
pub async fn flush(
&self,
storage_arcs: Vec<DhtArc>,
) -> SourceChainResult<(Vec<SignedActionHashed>, u32)> {
if self.scratch.apply(|s| s.is_empty())? {
return Ok((Vec::new(), 0));
}
let (scheduled_fns, actions, ops, entries, records, warrants) =
self.scratch.apply_and_then(|scratch| {
let records: Vec<Record> = scratch.records().collect();
let ops: Vec<HashedChainOp> =
records.iter().flat_map(produce_ops_from_record).collect();
let actions = scratch.drain_actions().collect::<Vec<_>>();
let entries = scratch.drain_entries().collect::<Vec<_>>();
let scheduled_fns = scratch.drain_scheduled_fns().collect::<Vec<_>>();
let warrants = scratch.drain_warrants().collect::<Vec<_>>();
SourceChainResult::Ok((scheduled_fns, actions, ops, entries, records, warrants))
})?;
let maybe_countersigned_entry = entries
.iter()
.map(|entry| entry.as_content())
.find(|entry| matches!(entry, Entry::CounterSign(_, _)));
if matches!(maybe_countersigned_entry, Some(Entry::CounterSign(_, _))) && actions.len() != 1
{
return Err(SourceChainError::DirtyCounterSigningWrite);
}
let lock_subject = chain_lock_subject_for_entry(maybe_countersigned_entry)?;
let is_countersigning_session = !lock_subject.is_empty();
let author = self.author.clone();
let persisted_head = self.head_info.as_ref().map(|h| h.action.clone());
let now = Timestamp::now();
let chain_flush_result: SourceChainResult<Vec<SignedActionHashed>> = async {
let _chain_write_permit = self
.dht_store
.acquire_chain_write_permit(author.as_ref())
.await;
if !records.is_empty() {
let chain_lock = self
.dht_store
.as_read()
.get_chain_lock(author.as_ref().clone())
.await?;
match chain_lock {
Some(chain_lock) => {
if chain_lock.subject() != lock_subject {
return Err(SourceChainError::ChainLocked);
}
else if chain_lock.is_expired_at(now) {
return Err(SourceChainError::LockExpired);
}
}
None => {
if is_countersigning_session {
return Err(SourceChainError::CountersigningWriteWithoutSession);
}
}
}
}
if !actions.is_empty() {
let head_info = self
.dht_store
.as_read()
.chain_head_for_author(author.as_ref())
.await?;
let latest_head = head_info.as_ref().map(|h| h.action.clone());
if persisted_head != latest_head {
return Err(SourceChainError::HeadMoved(
Box::new(actions),
Box::new(entries),
persisted_head,
head_info,
));
}
}
let mut tx = self
.dht_store
.db()
.begin()
.await
.map_err(SourceChainError::other)?;
let private_entry_hashes = actions
.iter()
.filter_map(|sah| {
let action = sah.action();
let visibility = action.entry_visibility()?;
if *visibility == EntryVisibility::Private {
action.entry_hash().cloned()
} else {
None
}
})
.collect::<std::collections::HashSet<_>>();
for entry_hashed in &entries {
let entry_hash = entry_hashed.as_hash();
let entry = entry_hashed.as_content();
if private_entry_hashes.contains(entry_hash) {
tx.insert_private_entry(entry_hash, author.as_ref(), entry)
.await
.map_err(SourceChainError::other)?;
} else {
tx.insert_entry(entry_hash, entry)
.await
.map_err(SourceChainError::other)?;
}
}
let mut inserted_action_hashes = std::collections::HashSet::<ActionHash>::new();
for sah in &actions {
tx.insert_action(sah, Some(RecordValidity::Accepted))
.await
.map_err(SourceChainError::other)?;
inserted_action_hashes.insert(sah.as_hash().clone());
crate::dht_store::action_indexes::insert_action_indexes(
&mut tx,
sah.as_hash(),
&sah.hashed.content.data,
)
.await
.map_err(SourceChainError::other)?;
if let Some((cap_access, tag)) = cap_grant_index_params(sah, &entries) {
tx.insert_cap_grant(sah.as_hash(), cap_access, tag.as_deref())
.await
.map_err(SourceChainError::other)?;
}
}
for op in &ops {
if !inserted_action_hashes.contains(op.action_hash()) {
continue;
}
let storage_center_loc = op.storage_center_loc;
let timestamp = op.action.action().timestamp();
let serialized_size = encoded_chain_op_size(op, &entries);
tx.insert_chain_op(holochain_data::dht::InsertChainOp {
op_hash: &op.op_hash,
action_hash: op.action_hash(),
op_type: i64::from(op.op_type),
basis_hash: &op.basis_hash,
storage_center_loc,
validation_status: RecordValidity::Accepted,
locally_validated: true,
require_receipt: false,
when_received: timestamp,
when_integrated: timestamp,
serialized_size,
})
.await
.map_err(SourceChainError::other)?;
let withhold = if is_countersigning_session {
Some(true)
} else {
None
};
tx.insert_chain_op_publish(&op.op_hash, None, None, withhold)
.await
.map_err(SourceChainError::other)?;
}
for scheduled_fn in &scheduled_fns {
let maybe_schedule_blob =
serialize_maybe_schedule_none().map_err(SourceChainError::other)?;
let _ = tx
.upsert_scheduled_function(holochain_data::dht::InsertScheduledFunction {
author: author.as_ref(),
zome_name: scheduled_fn.zome_name().0.as_ref(),
scheduled_fn: scheduled_fn.fn_name().0.as_ref(),
maybe_schedule: &maybe_schedule_blob,
start_at: now,
end_at: Timestamp::max(),
ephemeral: true,
})
.await
.map_err(SourceChainError::other)?;
}
tx.commit().await.map_err(SourceChainError::other)?;
SourceChainResult::Ok(actions)
}
.await;
match chain_flush_result {
Err(SourceChainError::HeadMoved(actions, entries, old_head, Some(new_head_info))) => {
let is_relaxed =
self.scratch
.apply_and_then::<bool, SyncScratchError, _>(|scratch| {
Ok(scratch.chain_top_ordering() == ChainTopOrdering::Relaxed)
})?;
if is_relaxed {
let keystore = self.keystore.clone();
let child_chain = Self::new(
self.dht_store.clone(),
keystore.clone(),
(*self.author).clone(),
)
.await?;
let rebased_actions =
rebase_actions_on(&keystore, *actions, new_head_info).await?;
child_chain.scratch.apply(move |scratch| {
for action in rebased_actions {
scratch.add_action(action, ChainTopOrdering::Relaxed);
}
for entry in *entries {
scratch.add_entry(entry, ChainTopOrdering::Relaxed);
}
})?;
child_chain.flush(storage_arcs).await
} else {
Err(SourceChainError::HeadMoved(
actions,
entries,
old_head,
Some(new_head_info),
))
}
}
Ok(actions) => {
let mut warrant_ops = Vec::new();
for warrant in warrants {
match warrant
.author
.verify_signature(warrant.signature(), warrant.data())
.await
{
Ok(true) => warrant_ops.push(DhtOpHashed::from_content_sync(
DhtOp::WarrantOp(Box::new(WarrantOp::from(warrant))),
)),
Ok(false) => {
tracing::info!(
"Invalid signature of a warrant in the scratch space. Skipping warrant"
);
continue;
}
Err(err) => {
tracing::warn!(?err, "Could not verify warrant signature before recording from scratch space into the DhtStore. Skipping warrant");
continue;
}
}
}
let total_warrants = warrant_ops.len() as u32;
if !warrant_ops.is_empty() {
let warrant_ops_with_validation_receipt_required_flag =
warrant_ops.into_iter().map(|op| (op, false)).collect();
self.dht_store
.record_incoming_ops(warrant_ops_with_validation_receipt_required_flag)
.await
.map_err(SourceChainError::other)?;
}
SourceChainResult::Ok((actions, total_warrants))
}
Err(e) => Err(e),
}
}
pub async fn valid_create_agent_key_action(
&self,
) -> SourceChainResult<holochain_zome_types::prelude::Action> {
let agent_key = self.agent_pubkey().clone();
self.dht_store
.as_read()
.valid_create_agent_key_action(&agent_key)
.await?
.ok_or_else(|| {
SourceChainError::InvalidAgentKey(agent_key, self.cell_id().as_ref().clone())
})
}
pub async fn delete_valid_agent_pub_key(&self) -> SourceChainResult<()> {
let valid_create_agent_key_action = self.valid_create_agent_key_action().await?;
self.put(
ActionData::Delete(DeleteData {
deletes_address: valid_create_agent_key_action.to_hash(),
deletes_entry_address: self.agent_pubkey().clone().into(),
}),
None,
ChainTopOrdering::Strict,
)
.await?;
Ok(())
}
}
impl SourceChain<DbWrite<Dht>> {
pub async fn new(
dht_store: DhtStore,
keystore: MetaLairClient,
author: AgentPubKey,
) -> SourceChainResult<Self> {
let scratch = Scratch::new().into_sync();
let author = Arc::new(author);
let cell_id = Arc::new(CellId::new(
dht_store.dna_hash().clone(),
author.as_ref().clone(),
));
let head_info = Some(
dht_store
.as_read()
.chain_head_for_author(author.as_ref())
.await?
.ok_or(SourceChainError::ChainEmpty)?,
);
Ok(Self {
scratch,
dht_store,
keystore,
author,
cell_id,
head_info,
public_only: false,
zomes_initialized: Arc::new(AtomicBool::new(false)),
})
}
pub async fn raw_empty(
dht_store: DhtStore,
keystore: MetaLairClient,
author: AgentPubKey,
) -> SourceChainResult<Self> {
let scratch = Scratch::new().into_sync();
let author = Arc::new(author);
let cell_id = Arc::new(CellId::new(
dht_store.dna_hash().clone(),
author.as_ref().clone(),
));
let head_info = dht_store
.as_read()
.chain_head_for_author(author.as_ref())
.await?;
Ok(Self {
scratch,
dht_store,
keystore,
author,
cell_id,
head_info,
public_only: false,
zomes_initialized: Arc::new(AtomicBool::new(false)),
})
}
pub fn as_read(&self) -> SourceChainRead {
SourceChain {
scratch: self.scratch.clone(),
dht_store: self.dht_store.as_read(),
keystore: self.keystore.clone(),
author: self.author.clone(),
cell_id: self.cell_id.clone(),
head_info: self.head_info.clone(),
public_only: self.public_only,
zomes_initialized: self.zomes_initialized.clone(),
}
}
}
impl<Db> SourceChain<Db>
where
Db: AsRef<DbRead<Dht>>,
{
pub fn public_only(&mut self) {
self.public_only = true;
}
pub fn keystore(&self) -> &MetaLairClient {
&self.keystore
}
pub fn snapshot(&self) -> SourceChainResult<Scratch> {
Ok(self.scratch.apply(|scratch| scratch.clone())?)
}
pub fn scratch(&self) -> SyncScratch {
self.scratch.clone()
}
pub fn agent_pubkey(&self) -> &AgentPubKey {
self.author.as_ref()
}
pub fn to_agent_pubkey(&self) -> Arc<AgentPubKey> {
self.author.clone()
}
pub fn cell_id(&self) -> Arc<CellId> {
self.cell_id.clone()
}
pub fn scratch_records(&self) -> SourceChainResult<Vec<Record>> {
Ok(self.scratch.apply(|scratch| scratch.records().collect())?)
}
pub async fn zomes_initialized(&self) -> SourceChainResult<bool> {
if self.zomes_initialized.load(Ordering::Relaxed) {
return Ok(true);
}
let query_filter = ChainQueryFilter {
action_type: Some(vec![ActionType::InitZomesComplete]),
..QueryFilter::default()
};
let init_zomes_complete_actions = self.query(query_filter).await?;
if init_zomes_complete_actions.len() > 1 {
tracing::warn!("Multiple InitZomesComplete actions are present");
}
let zomes_initialized = !init_zomes_complete_actions.is_empty();
self.set_zomes_initialized(zomes_initialized);
Ok(zomes_initialized)
}
pub fn set_zomes_initialized(&self, value: bool) {
self.zomes_initialized.store(value, Ordering::Relaxed);
}
pub fn persisted_head_info(&self) -> Option<HeadInfo> {
self.head_info.clone()
}
pub fn chain_head(&self) -> SourceChainResult<Option<HeadInfo>> {
Ok(self
.scratch
.apply(|scratch| scratch.chain_head().or_else(|| self.persisted_head_info()))?)
}
pub fn chain_head_nonempty(&self) -> SourceChainResult<HeadInfo> {
self.chain_head()?.ok_or(SourceChainError::ChainEmpty)
}
#[cfg(feature = "test_utils")]
pub fn len(&self) -> SourceChainResult<u32> {
Ok(self.scratch.apply(|scratch| {
let scratch_max = scratch.chain_head().map(|h| h.seq);
let persisted_max = self.head_info.as_ref().map(|h| h.seq);
match (scratch_max, persisted_max) {
(None, None) => 0,
(Some(s), None) => s + 1,
(None, Some(s)) => s + 1,
(Some(a), Some(b)) => a.max(b) + 1,
}
})?)
}
#[cfg(feature = "test_utils")]
pub fn is_empty(&self) -> SourceChainResult<bool> {
Ok(self.len()? == 0)
}
pub async fn valid_cap_grant(
&self,
check_function: GrantedFunction,
check_agent: AgentPubKey,
check_secret: Option<CapSecret>,
) -> SourceChainResult<Option<CapGrant>> {
let author_grant = CapGrant::from(self.agent_pubkey().clone());
if author_grant.is_valid(&check_function, &check_agent, check_secret.as_ref()) {
return Ok(Some(author_grant));
}
let cap_grants = self
.dht_store
.as_read()
.valid_cap_grants(self.agent_pubkey(), check_secret.as_ref())
.await?;
for cap_grant in cap_grants {
if cap_grant.is_valid(&check_function, &check_agent, check_secret.as_ref()) {
return Ok(Some(cap_grant));
}
}
Ok(None)
}
pub async fn query(
&self,
query: QueryFilter,
) -> SourceChainResult<Vec<holochain_zome_types::prelude::Record>> {
let public_only = self.public_only;
let mut records = self
.dht_store
.as_read()
.source_chain_records(self.author.as_ref(), query.include_entries, public_only)
.await?;
if query.order_descending {
records.reverse();
}
self.scratch.apply(|scratch| {
let mut scratch_records: Vec<_> = scratch
.actions()
.filter_map(|sah| {
let entry = match sah.action().entry_hash() {
Some(eh) if query.include_entries => scratch.get_entry(eh).ok()?,
_ => None,
};
let record_entry = RecordEntry::new(sah.action().entry_visibility(), entry);
Some(Record::new(sah.clone(), record_entry))
})
.collect();
scratch_records.sort_unstable_by_key(|e| e.action().action_seq());
records.extend(scratch_records);
})?;
Ok(query.filter_records(records))
}
pub async fn get_chain_lock(&self) -> SourceChainResult<Option<ChainLock>> {
Ok(self
.dht_store
.as_read()
.get_chain_lock(self.author.as_ref().clone())
.await?)
}
pub fn countersigning_op(&self) -> SourceChainResult<Option<ChainOp>> {
let r = self.scratch.apply(|scratch| {
scratch
.entries()
.find(|e| matches!(**e.1, Entry::CounterSign(_, _)))
.and_then(|(entry_hash, entry)| {
scratch
.actions()
.find(|shh| {
shh.action()
.entry_hash()
.map(|eh| eh == entry_hash)
.unwrap_or(false)
})
.map(|shh| {
let signed_action =
SignedAction::new(shh.action().clone(), shh.signature().clone());
ChainOp::CreateEntry(signed_action, OpEntry::Present((**entry).clone()))
})
})
})?;
Ok(r)
}
pub async fn dump(&self) -> SourceChainResult<SourceChainDump> {
dump_state(&self.dht_store.as_read(), (*self.author).clone()).await
}
}
pub fn chain_lock_subject_for_entry(entry: Option<&Entry>) -> SourceChainResult<Vec<u8>> {
Ok(match entry {
Some(Entry::CounterSign(session_data, _)) => holo_hash::encode::blake2b_256(
&holochain_serialized_bytes::encode(session_data.preflight_request())?,
),
_ => Vec::with_capacity(0),
})
}
async fn rebase_actions_on(
keystore: &MetaLairClient,
mut actions: Vec<SignedActionHashed>,
mut head: HeadInfo,
) -> Result<Vec<SignedActionHashed>, ScratchError> {
actions.sort_by_key(|shh| shh.action().action_seq());
for shh in actions.iter_mut() {
let mut action = shh.action().clone();
holochain_zome_types::action::ActionExt::rebase_on(
&mut action,
head.action.clone(),
head.seq,
head.timestamp,
)?;
head.seq = action.action_seq();
head.timestamp = action.timestamp();
let hh = HoloHashed::<Action>::from_content_sync(action);
head.action = hh.as_hash().clone();
let new_shh = SignedActionHashed::sign(keystore, hh).await?;
*shh = new_shh;
}
Ok(actions)
}
#[cfg_attr(feature = "instrument", tracing::instrument(skip_all))]
pub async fn genesis(
dht_store: DhtStore,
keystore: MetaLairClient,
dna_hash: DnaHash,
agent_pubkey: AgentPubKey,
membrane_proof: Option<MembraneProof>,
) -> SourceChainResult<()> {
let dna_header = ActionHeader {
author: agent_pubkey.clone(),
timestamp: Timestamp::now(),
action_seq: 0,
prev_action: None,
};
let dna_action = build_action(dna_header, ActionData::Dna(DnaData { dna_hash }));
let dna_action_hashed = HoloHashed::<Action>::from_content_sync(dna_action);
let dna_action = SignedActionHashed::sign(&keystore, dna_action_hashed).await?;
let dna_action_address = dna_action.as_hash().clone();
let dna_ops = produce_ops_from_record(&Record::new(dna_action.clone(), RecordEntry::NA));
let agent_validation_header = ActionHeader {
author: agent_pubkey.clone(),
timestamp: Timestamp::now(),
action_seq: 1,
prev_action: Some(dna_action_address),
};
let agent_validation_action = build_action(
agent_validation_header,
ActionData::AgentValidationPkg(AgentValidationPkgData { membrane_proof }),
);
let agent_validation_action_hashed =
HoloHashed::<Action>::from_content_sync(agent_validation_action);
let agent_validation_action =
SignedActionHashed::sign(&keystore, agent_validation_action_hashed).await?;
let avh_addr = agent_validation_action.as_hash().clone();
let avh_ops = produce_ops_from_record(&Record::new(
agent_validation_action.clone(),
RecordEntry::NA,
));
let agent_header = ActionHeader {
author: agent_pubkey.clone(),
timestamp: Timestamp::now(),
action_seq: 2,
prev_action: Some(avh_addr),
};
let agent_action = build_action(
agent_header,
ActionData::Create(CreateData {
entry_type: EntryType::AgentPubKey,
entry_hash: agent_pubkey.clone().into(),
}),
);
let agent_action_hashed = HoloHashed::<Action>::from_content_sync(agent_action);
let agent_action = SignedActionHashed::sign(&keystore, agent_action_hashed).await?;
let agent_entry = Some(Entry::Agent(agent_pubkey.clone()));
let agent_ops = produce_ops_from_record(&Record::new(
agent_action.clone(),
RecordEntry::new(
agent_action.action().entry_visibility(),
agent_entry.clone(),
),
));
let ops_with_hashes_for_new_db: Vec<HashedChainOp> = dna_ops
.into_iter()
.chain(avh_ops)
.chain(agent_ops)
.collect();
let dna_action_for_new_db = dna_action.clone();
let agent_validation_action_for_new_db = agent_validation_action.clone();
let agent_action_for_new_db = agent_action.clone();
let agent_entry_for_new_db = agent_entry.clone();
let agent_entry_hash: EntryHash = agent_pubkey.into();
{
let mut tx = dht_store
.db()
.begin()
.await
.map_err(SourceChainError::other)?;
if let Some(entry) = &agent_entry_for_new_db {
tx.insert_entry(&agent_entry_hash, entry)
.await
.map_err(SourceChainError::other)?;
}
let genesis_actions: &[&SignedActionHashed] = &[
&dna_action_for_new_db,
&agent_validation_action_for_new_db,
&agent_action_for_new_db,
];
for sah in genesis_actions {
tx.insert_action(sah, Some(RecordValidity::Accepted))
.await
.map_err(SourceChainError::other)?;
}
let genesis_entries_slice: Vec<EntryHashed> = agent_entry_for_new_db
.as_ref()
.map(|e| {
vec![EntryHashed::with_pre_hashed(
e.clone(),
agent_entry_hash.clone(),
)]
})
.unwrap_or_default();
for op in &ops_with_hashes_for_new_db {
let storage_center_loc = op.storage_center_loc;
let timestamp = op.action.action().timestamp();
let serialized_size = encoded_chain_op_size(op, &genesis_entries_slice);
tx.insert_chain_op(holochain_data::dht::InsertChainOp {
op_hash: &op.op_hash,
action_hash: op.action_hash(),
op_type: i64::from(op.op_type),
basis_hash: &op.basis_hash,
storage_center_loc,
validation_status: RecordValidity::Accepted,
locally_validated: true,
require_receipt: false,
when_received: timestamp,
when_integrated: timestamp,
serialized_size,
})
.await
.map_err(SourceChainError::other)?;
tx.insert_chain_op_publish(&op.op_hash, None, None, None)
.await
.map_err(SourceChainError::other)?;
}
tx.commit().await.map_err(SourceChainError::other)?;
}
Ok(())
}
pub type CurrentCountersigningSessionOpt = Option<(Record, EntryHash, CounterSigningSessionData)>;
#[cfg_attr(feature = "instrument", tracing::instrument(skip_all))]
pub async fn dump_state(
dht_store: &DhtStoreRead,
author: AgentPubKey,
) -> Result<SourceChainDump, SourceChainError> {
dump_state_paginated(dht_store, author, None, None).await
}
#[cfg_attr(feature = "instrument", tracing::instrument(skip_all))]
pub async fn dump_state_paginated(
dht_store: &DhtStoreRead,
author: AgentPubKey,
cursor: Option<&SourceChainCursor>,
limit: Option<u32>,
) -> Result<SourceChainDump, SourceChainError> {
dht_store
.dump_source_chain_paginated(&author, cursor, limit)
.await
.map_err(SourceChainError::other)
}
fn cap_grant_index_params(
shh: &SignedActionHashed,
entries: &[EntryHashed],
) -> Option<(i64, Option<String>)> {
let (entry_type, entry_hash) = match &shh.action().data {
ActionData::Create(d) => (&d.entry_type, &d.entry_hash),
ActionData::Update(d) => (&d.entry_type, &d.entry_hash),
_ => return None,
};
if !matches!(entry_type, EntryType::CapGrant) {
return None;
}
let entry = entries
.iter()
.find(|e| e.as_hash() == entry_hash)?
.as_content();
let cap_grant = match entry {
Entry::CapGrant(g) => g,
_ => return None,
};
let cap_access_i64 = match &cap_grant.access {
CapAccess::Unrestricted => 0_i64,
CapAccess::Transferable { .. } => 1_i64,
CapAccess::Assigned { .. } => 2_i64,
};
let tag = if cap_grant.tag.is_empty() {
None
} else {
Some(cap_grant.tag.clone())
};
Some((cap_access_i64, tag))
}
fn serialize_maybe_schedule_none(
) -> Result<Vec<u8>, holochain_serialized_bytes::SerializedBytesError> {
holochain_serialized_bytes::encode(&None::<holochain_zome_types::schedule::Schedule>)
}
pub(crate) fn encoded_chain_op_size(op: &HashedChainOp, entries: &[EntryHashed]) -> u32 {
let action = op.action.action();
let maybe_entry: Option<Entry> = action
.entry_hash()
.and_then(|eh| entries.iter().find(|e| e.as_hash() == eh))
.map(|e| e.as_content().clone());
let signed_action = SignedAction::new(action.clone(), op.action.signature().clone());
let op_entry = |entry: Option<Entry>| match entry {
Some(entry) => OpEntry::Present(entry),
None => OpEntry::ActionOnly,
};
let chain_op = ChainOp::from_type(op.op_type, signed_action, op_entry(maybe_entry));
holochain_serialized_bytes::encode(&DhtOp::ChainOp(Box::new(chain_op)))
.map(|b| b.len() as u32)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source_chain::SourceChainResult;
use ::fixt::fixt;
use ::fixt::prelude::*;
use holo_hash::fixt::DnaHashFixturator;
use holo_hash::fixt::{ActionHashFixturator, AgentPubKeyFixturator, EntryHashFixturator};
use holochain_keystore::test_keystore;
use holochain_zome_types::prelude::{CloseChainData, Entry, InitZomesCompleteData, UpdateData};
use matches::assert_matches;
use std::collections::{BTreeSet, HashSet};
#[tokio::test(flavor = "multi_thread")]
async fn test_relaxed_ordering() -> SourceChainResult<()> {
let TestCase {
chain: chain_1,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let chain_2 = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let chain_3 = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let close_chain = ActionData::CloseChain(CloseChainData { new_target: None });
chain_1
.put(close_chain.clone(), None, ChainTopOrdering::Strict)
.await?;
chain_2
.put(close_chain.clone(), None, ChainTopOrdering::Strict)
.await?;
chain_3
.put(close_chain, None, ChainTopOrdering::Relaxed)
.await?;
let storage_arcs = vec![DhtArc::Empty];
chain_1.flush(storage_arcs.clone()).await?;
let seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush")
.seq;
assert_eq!(seq, 3);
assert!(matches!(
chain_2.flush(storage_arcs.clone()).await,
Err(SourceChainError::HeadMoved(_, _, _, _))
));
let seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush")
.seq;
assert_eq!(seq, 3);
chain_3.flush(storage_arcs).await?;
let seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush")
.seq;
assert_eq!(seq, 4);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_relaxed_ordering_with_entry() -> SourceChainResult<()> {
let TestCase {
chain: chain_1,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let chain_2 = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let chain_3 = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let entry_1 = Entry::App(fixt!(AppEntryBytes));
let eh1 = EntryHash::with_data_sync(&entry_1);
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(fixt!(AppEntryDef)),
entry_hash: eh1.clone(),
});
let h1 = chain_1
.put(create, Some(entry_1.clone()), ChainTopOrdering::Strict)
.await
.unwrap();
let entry_err = Entry::App(fixt!(AppEntryBytes));
let entry_hash_err = EntryHash::with_data_sync(&entry_err);
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(fixt!(AppEntryDef)),
entry_hash: entry_hash_err.clone(),
});
chain_2
.put(create, Some(entry_err.clone()), ChainTopOrdering::Strict)
.await
.unwrap();
let entry_2 = Entry::App(fixt!(AppEntryBytes));
let eh2 = EntryHash::with_data_sync(&entry_2);
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(AppEntryDef::new(
EntryDefIndex(0),
0.into(),
EntryVisibility::Private,
)),
entry_hash: eh2.clone(),
});
let old_h2 = chain_3
.put(create, Some(entry_2.clone()), ChainTopOrdering::Relaxed)
.await
.unwrap();
let storage_arcs = vec![DhtArc::Empty];
chain_1.flush(storage_arcs.clone()).await?;
let seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush")
.seq;
assert_eq!(seq, 3);
assert!(matches!(
chain_2.flush(storage_arcs.clone()).await,
Err(SourceChainError::HeadMoved(_, _, _, _))
));
chain_3.flush(storage_arcs).await?;
let head = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush");
assert_ne!(head.action, old_h2);
assert_eq!(head.seq, 4);
let h1_record_entry_fetched = dht_store
.as_read()
.retrieve_record(&h1, Some(&alice))
.await?
.expect("h1 record present in store")
.into_inner()
.1;
let h2_record_entry_fetched = dht_store
.as_read()
.retrieve_record(&head.action, Some(&alice))
.await?
.expect("h2 record present in store")
.into_inner()
.1;
assert_eq!(RecordEntry::Present(entry_1), h1_record_entry_fetched);
assert_eq!(RecordEntry::Present(entry_2), h2_record_entry_fetched);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn valid_create_agent_key_action_reads_from_store() {
let TestCase {
chain, agent_key, ..
} = TestCase::new().await;
let action = chain.valid_create_agent_key_action().await.unwrap();
assert_matches!(action.data, ActionData::Create(_));
assert_eq!(action.entry_type(), Some(&EntryType::AgentPubKey));
let agent_key_entry_hash: EntryHash = agent_key.into();
assert_eq!(action.entry_hash(), Some(&agent_key_entry_hash));
}
#[tokio::test(flavor = "multi_thread")]
async fn delete_valid_agent_pub_key() {
let TestCase { chain, .. } = TestCase::new().await;
let result = chain.delete_valid_agent_pub_key().await;
assert!(result.is_ok());
chain.flush(vec![DhtArc::Empty]).await.unwrap();
let result = chain.delete_valid_agent_pub_key().await.unwrap_err();
assert_matches!(result, SourceChainError::InvalidAgentKey(invalid_key, cell_id) if invalid_key == *chain.author && cell_id == *chain.cell_id());
}
#[tokio::test(flavor = "multi_thread")]
async fn updated_agent_key_is_invalid() {
let TestCase {
chain, agent_key, ..
} = TestCase::new().await;
let create = chain.valid_create_agent_key_action().await.unwrap();
let agent_key_entry_hash: EntryHash = agent_key.clone().into();
let action_data = ActionData::Update(UpdateData {
entry_type: EntryType::AgentPubKey,
entry_hash: agent_key_entry_hash.clone(),
original_action_address: create.to_hash(),
original_entry_address: agent_key_entry_hash,
});
chain
.put(
action_data,
Some(Entry::Agent(agent_key.clone())),
ChainTopOrdering::default(),
)
.await
.unwrap();
chain.flush(vec![DhtArc::Empty]).await.unwrap();
let result = chain.valid_create_agent_key_action().await.unwrap_err();
assert_matches!(
result,
SourceChainError::InvalidAgentKey(invalid_key, _) if invalid_key == agent_key
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_cap_grant() -> SourceChainResult<()> {
let TestCase {
chain,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let secret = Some(CapSecretFixturator::new(Unpredictable).next().unwrap());
#[allow(clippy::unnecessary_literal_unwrap)] let secret_access = CapAccess::from(secret.unwrap());
let _curry = CurryPayloadsFixturator::new(Empty).next().unwrap();
let function: GrantedFunction = ("foo".into(), "bar".into());
let mut fns = HashSet::new();
fns.insert(function.clone());
let functions = GrantedFunctions::Listed(fns);
let grant = ZomeCallCapGrant::new("tag".into(), secret_access.clone(), functions.clone());
let bob = keystore.new_sign_keypair_random().await.unwrap();
let carol = keystore.new_sign_keypair_random().await.unwrap();
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), secret)
.await?,
None
);
let storage_arcs = vec![DhtArc::Empty];
let (original_action_address, original_entry_address) = {
let (entry, entry_hash) =
EntryHashed::from_content_sync(Entry::CapGrant(grant.clone())).into_inner();
let action_data = ActionData::Create(CreateData {
entry_type: EntryType::CapGrant,
entry_hash: entry_hash.clone(),
});
let action = chain
.put(action_data, Some(entry), ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs.clone()).await.unwrap();
(action, entry_hash)
};
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), secret)
.await?,
Some(grant.clone().into())
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), carol.clone(), secret)
.await?,
Some(grant.clone().into())
);
assert_eq!(
chain
.valid_cap_grant(("boo".into(), "far".into()), bob.clone(), secret)
.await?,
None
);
let mut assignees = BTreeSet::new();
assignees.insert(bob.clone());
let updated_secret = Some(CapSecretFixturator::new(Unpredictable).next().unwrap());
#[allow(clippy::unnecessary_literal_unwrap)] let updated_access = CapAccess::from((updated_secret.unwrap(), assignees));
let updated_grant = ZomeCallCapGrant::new("tag".into(), updated_access.clone(), functions);
let (updated_action_hash, updated_entry_hash) = {
let chain =
SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let (entry, entry_hash) =
EntryHashed::from_content_sync(Entry::CapGrant(updated_grant.clone())).into_inner();
let action_data = ActionData::Update(UpdateData {
entry_type: EntryType::CapGrant,
entry_hash: entry_hash.clone(),
original_action_address,
original_entry_address,
});
let action = chain
.put(action_data, Some(entry), ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs.clone()).await.unwrap();
(action, entry_hash)
};
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), updated_secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), secret)
.await?,
None
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), updated_secret)
.await?,
Some(updated_grant.clone().into())
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), carol.clone(), secret)
.await?,
None
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), carol.clone(), updated_secret)
.await?,
None
);
{
let extra_dht_store = crate::test_utils::test_dht_store(fake_dna_hash(1)).await;
genesis(
extra_dht_store.clone(),
keystore.clone(),
fake_dna_hash(1),
carol.clone(),
None,
)
.await
.unwrap();
let carol_chain = SourceChain::new(extra_dht_store, keystore.clone(), carol.clone())
.await
.unwrap();
let maybe_cap_grant = carol_chain
.valid_cap_grant(("".into(), "".into()), alice.clone(), secret)
.await
.unwrap();
assert_eq!(maybe_cap_grant, None);
}
{
let chain =
SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let action_data = ActionData::Delete(DeleteData {
deletes_address: updated_action_hash,
deletes_entry_address: updated_entry_hash,
});
chain
.put(action_data, None, ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs.clone()).await.unwrap();
}
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), alice.clone(), updated_secret)
.await?,
Some(CapGrant::ChainAuthor(alice.clone())),
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), secret)
.await?,
None
);
assert_eq!(
chain
.valid_cap_grant(function.clone(), bob.clone(), updated_secret)
.await?,
None
);
let unrestricted_grant = ZomeCallCapGrant::new(
"unrestricted".into(),
CapAccess::Unrestricted,
GrantedFunctions::All,
);
let (original_action_address, original_entry_address) = {
let chain =
SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let (entry, entry_hash) =
EntryHashed::from_content_sync(Entry::CapGrant(unrestricted_grant.clone()))
.into_inner();
let action_data = ActionData::Create(CreateData {
entry_type: EntryType::CapGrant,
entry_hash: entry_hash.clone(),
});
let action = chain
.put(action_data, Some(entry), ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs.clone()).await.unwrap();
(action, entry_hash)
};
let granted_function: GrantedFunction = ("zome".into(), "fn".into());
assert_eq!(
chain
.valid_cap_grant(granted_function.clone(), bob.clone(), None)
.await?,
Some(unrestricted_grant.clone().into())
);
assert_eq!(
chain
.valid_cap_grant(granted_function.clone(), carol.clone(), None)
.await?,
Some(unrestricted_grant.clone().into())
);
{
{
let extra_dht_store = crate::test_utils::test_dht_store(fake_dna_hash(1)).await;
genesis(
extra_dht_store.clone(),
keystore.clone(),
fake_dna_hash(1),
bob.clone(),
None,
)
.await
.unwrap();
let bob_chain = SourceChain::new(extra_dht_store, keystore.clone(), bob.clone())
.await
.unwrap();
let maybe_cap_grant = bob_chain
.valid_cap_grant(("".into(), "".into()), carol.clone(), None)
.await
.unwrap();
assert_eq!(maybe_cap_grant, None);
}
}
{
let chain =
SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let action_data = ActionData::Delete(DeleteData {
deletes_address: original_action_address,
deletes_entry_address: original_entry_address,
});
chain
.put(action_data, None, ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs.clone()).await.unwrap();
}
assert_eq!(
chain
.valid_cap_grant(granted_function.clone(), bob.clone(), None)
.await?,
None
);
let some_zome_name: ZomeName = "some_zome".into();
let some_fn_name: FunctionName = "some_fn".into();
let mut granted_fns = HashSet::new();
granted_fns.insert((some_zome_name.clone(), some_fn_name.clone()));
let first_unrestricted_grant = ZomeCallCapGrant::new(
"unrestricted_1".into(),
CapAccess::Unrestricted,
GrantedFunctions::Listed(granted_fns),
);
let granted_zome_name: ZomeName = "granted_zome".into();
let granted_fn_name: FunctionName = "granted_fn".into();
let mut granted_fns = HashSet::new();
granted_fns.insert((granted_zome_name.clone(), granted_fn_name.clone()));
let second_unrestricted_grant = ZomeCallCapGrant::new(
"unrestricted_2".into(),
CapAccess::Unrestricted,
GrantedFunctions::Listed(granted_fns),
);
{
let chain =
SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let (entry, entry_hash) =
EntryHashed::from_content_sync(Entry::CapGrant(first_unrestricted_grant.clone()))
.into_inner();
let action_data = ActionData::Create(CreateData {
entry_type: EntryType::CapGrant,
entry_hash: entry_hash.clone(),
});
let _ = chain
.put(action_data, Some(entry), ChainTopOrdering::default())
.await?;
let (entry, entry_hash) =
EntryHashed::from_content_sync(Entry::CapGrant(second_unrestricted_grant.clone()))
.into_inner();
let action_data = ActionData::Create(CreateData {
entry_type: EntryType::CapGrant,
entry_hash: entry_hash.clone(),
});
let _ = chain
.put(action_data, Some(entry), ChainTopOrdering::default())
.await?;
chain.flush(storage_arcs).await.unwrap();
}
let actual_cap_grant = chain
.valid_cap_grant((granted_zome_name, granted_fn_name), bob, None)
.await
.unwrap();
assert_eq!(actual_cap_grant, Some(second_unrestricted_grant.into()));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn source_chain_buffer_iter_back() -> SourceChainResult<()> {
holochain_trace::test_run();
let keystore = test_keystore();
let dna_hash = fixt!(DnaHash);
let dht_store = crate::test_utils::test_dht_store(dna_hash.clone()).await;
let author = Arc::new(keystore.new_sign_keypair_random().await.unwrap());
genesis(
dht_store.clone(),
keystore.clone(),
dna_hash,
(*author).clone(),
None,
)
.await
.unwrap();
let source_chain = SourceChain::new(dht_store.clone(), keystore.clone(), (*author).clone())
.await
.unwrap();
let entry = Entry::App(fixt!(AppEntryBytes));
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(fixt!(AppEntryDef)),
entry_hash: EntryHash::with_data_sync(&entry),
});
let h1 = source_chain
.put(create, Some(entry), ChainTopOrdering::default())
.await
.unwrap();
let entry = Entry::App(fixt!(AppEntryBytes));
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(fixt!(AppEntryDef)),
entry_hash: EntryHash::with_data_sync(&entry),
});
let h2 = source_chain
.put(create, Some(entry), ChainTopOrdering::default())
.await
.unwrap();
source_chain.flush(vec![DhtArc::Empty]).await.unwrap();
let head = dht_store
.as_read()
.chain_head_for_author(author.as_ref())
.await?
.expect("chain head present after flush");
assert_eq!(head.action, h2);
let h1_record_fetched = dht_store
.as_read()
.retrieve_record(&h1, Some(author.as_ref()))
.await?
.expect("h1 record present in store");
let h2_record_fetched = dht_store
.as_read()
.retrieve_record(&h2, Some(author.as_ref()))
.await?
.expect("h2 record present in store");
assert_eq!(h1, *h1_record_fetched.action_address());
assert_eq!(h2, *h2_record_fetched.action_address());
let source_chain = SourceChain::new(dht_store.clone(), keystore.clone(), (*author).clone())
.await
.unwrap();
let res = source_chain.query(QueryFilter::new()).await.unwrap();
assert_eq!(res.len(), 5);
assert_eq!(*res[3].action_address(), h1);
assert_eq!(*res[4].action_address(), h2);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn genesis_writes_to_merged_store() -> SourceChainResult<()> {
holochain_trace::test_run();
let keystore = test_keystore();
let dna_hash = fixt!(DnaHash);
let dht_store = crate::test_utils::test_dht_store(dna_hash.clone()).await;
let author = keystore.new_sign_keypair_random().await.unwrap();
genesis(
dht_store.clone(),
keystore.clone(),
dna_hash,
author.clone(),
None,
)
.await
.unwrap();
let store = dht_store.as_read();
assert!(store.has_genesis(&author).await?);
let head = store
.chain_head_for_author(&author)
.await?
.expect("chain head present after genesis");
assert_eq!(head.seq, 2);
let head_record = store
.retrieve_record(&head.action, Some(&author))
.await?
.expect("head record present in store");
assert_eq!(head_record.action().action_seq(), 2);
assert!(matches!(head_record.action().data, ActionData::Create(_)));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn dump_state_from_store() -> SourceChainResult<()> {
let TestCase {
chain,
agent_key,
dht_store,
keystore,
} = TestCase::new().await;
let private_entry = Entry::App(fixt!(AppEntryBytes));
let private_entry_hash = EntryHash::with_data_sync(&private_entry);
let create = ActionData::Create(CreateData {
entry_type: EntryType::App(AppEntryDef::new(
0.into(),
0.into(),
EntryVisibility::Private,
)),
entry_hash: private_entry_hash.clone(),
});
chain
.put(
create,
Some(private_entry.clone()),
ChainTopOrdering::default(),
)
.await?;
chain.flush(vec![DhtArc::Empty]).await?;
let dump = dht_store.as_read().dump_source_chain(&agent_key).await?;
assert_eq!(
dump.records.len(),
4,
"expected 4 records after genesis + 1"
);
for (i, rec) in dump.records.iter().enumerate() {
assert_eq!(rec.action.action_seq(), i as u32, "record {i} out of order");
}
let private_rec = &dump.records[3];
assert_eq!(
private_rec.entry.as_ref(),
Some(&private_entry),
"private-entry record must include the entry"
);
assert_eq!(
dump.published_ops_count, 0,
"no ops have been published yet"
);
let published_op = dht_store
.as_read()
.ops_to_publish_for_wire(&agent_key)
.await?
.into_iter()
.next()
.expect("at least one authored op");
dht_store
.record_published_op_hashes(
vec![DhtOpHash::from_raw_36(published_op.op_hash)],
Timestamp::now(),
)
.await?;
let first_page = dht_store
.as_read()
.dump_source_chain_paginated(&agent_key, None, Some(2))
.await?;
assert_eq!(first_page.records.len(), 2);
assert_eq!(first_page.records, dump.records[..2]);
assert_eq!(first_page.published_ops_count, 1);
let sequence_page = dht_store
.as_read()
.dump_source_chain_paginated(
&agent_key,
Some(&SourceChainCursor::Sequence(1)),
Some(10),
)
.await?;
let hash_page = dht_store
.as_read()
.dump_source_chain_paginated(
&agent_key,
Some(&SourceChainCursor::ActionHash(
dump.records[1].action_address.clone(),
)),
Some(10),
)
.await?;
assert_eq!(sequence_page, hash_page);
assert_eq!(sequence_page.records, dump.records[2..]);
assert_eq!(sequence_page.published_ops_count, 1);
let empty_page = dht_store
.as_read()
.dump_source_chain_paginated(&agent_key, Some(&SourceChainCursor::Sequence(3)), Some(5))
.await?;
assert!(empty_page.records.is_empty());
assert_eq!(empty_page.published_ops_count, 1);
assert!(dht_store
.as_read()
.dump_source_chain_paginated(&agent_key, None, Some(0))
.await
.is_err());
assert!(dht_store
.as_read()
.dump_source_chain_paginated(
&agent_key,
Some(&SourceChainCursor::ActionHash(ActionHash::from_raw_36(
vec![42; 36],
))),
Some(1),
)
.await
.is_err());
let mut rejected_action = dump.records[3].action.clone();
rejected_action.header.action_seq = 99;
rejected_action.header.prev_action = Some(dump.records[3].action_address.clone());
rejected_action.header.timestamp = Timestamp::now();
let rejected_action_hash = ActionHash::with_data_sync(&rejected_action);
let rejected_op =
DhtOpHashed::from_content_sync(DhtOp::ChainOp(Box::new(ChainOp::AgentActivity(
SignedAction::new(rejected_action, Signature::from([42; 64])),
))));
dht_store
.record_incoming_ops(vec![(rejected_op, false)])
.await?;
assert!(dht_store
.as_read()
.dump_source_chain_paginated(
&agent_key,
Some(&SourceChainCursor::ActionHash(rejected_action_hash)),
Some(1),
)
.await
.is_err());
let other_agent = keystore.new_sign_keypair_random().await.unwrap();
genesis(
dht_store.clone(),
keystore,
chain.cell_id().dna_hash().clone(),
other_agent.clone(),
None,
)
.await?;
let other_dump = dht_store.as_read().dump_source_chain(&other_agent).await?;
assert!(dht_store
.as_read()
.dump_source_chain_paginated(
&agent_key,
Some(&SourceChainCursor::ActionHash(
other_dump.records[0].action_address.clone(),
)),
Some(1),
)
.await
.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn source_chain_buffer_dump_entries_json() -> SourceChainResult<()> {
let TestCase {
chain: _,
agent_key,
dht_store,
..
} = TestCase::new().await;
let json = dump_state(&dht_store.as_read(), agent_key.clone()).await?;
let json = serde_json::to_string_pretty(&json)?;
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["records"][0]["action"]["data"]["type"], "Dna");
assert_eq!(parsed["records"][0]["entry"], serde_json::Value::Null);
assert_eq!(parsed["records"][2]["action"]["data"]["type"], "Create");
assert_eq!(
parsed["records"][2]["action"]["data"]["entry_type"],
"AgentPubKey"
);
assert_eq!(parsed["records"][2]["entry"]["entry_type"], "Agent");
assert_ne!(
parsed["records"][2]["entry"]["entry"],
serde_json::Value::Null
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn source_chain_query() {
let TestCase {
chain,
agent_key: alice,
keystore,
..
} = TestCase::new().await;
let app_entry_type = EntryType::App(AppEntryDef {
zome_index: 0.into(),
entry_index: 0.into(),
visibility: EntryVisibility::Public,
});
let chain_top = chain.chain_head_nonempty().unwrap();
let create_action = {
let entry = Entry::App(fixt!(AppEntryBytes));
let entry_hashed = EntryHashed::from_content_sync(entry);
let action = Action {
header: ActionHeader {
author: alice.clone(),
timestamp: Timestamp::now(),
action_seq: chain_top.seq + 1,
prev_action: Some(chain_top.action.as_hash().clone()),
},
data: ActionData::Create(CreateData {
entry_type: app_entry_type.clone(),
entry_hash: entry_hashed.hash.clone(),
}),
};
let sig = alice.sign(&keystore, &action).await.unwrap();
let signed_action = SignedActionHashed::with_presigned(
HoloHashed::from_content_sync(action.clone()),
sig,
);
chain
.scratch()
.apply(move |scratch| {
scratch.add_action(signed_action, ChainTopOrdering::Strict);
scratch.add_entry(entry_hashed, ChainTopOrdering::Strict);
})
.unwrap();
chain.flush(vec![DhtArc::Empty]).await.unwrap();
action
};
{
let chain_top = chain.chain_head_nonempty().unwrap();
let entry = Entry::App(fixt!(AppEntryBytes));
let entry_hashed = EntryHashed::from_content_sync(entry);
let action = Action {
header: ActionHeader {
author: alice.clone(),
timestamp: Timestamp::now(),
action_seq: chain_top.seq + 2,
prev_action: Some(create_action.to_hash()),
},
data: ActionData::Update(UpdateData {
original_action_address: create_action.to_hash(),
original_entry_address: create_action.entry_hash().unwrap().clone(),
entry_type: app_entry_type.clone(),
entry_hash: entry_hashed.hash.clone(),
}),
};
let sig = alice.sign(&keystore, &action).await.unwrap();
let signed_action =
SignedActionHashed::with_presigned(HoloHashed::from_content_sync(action), sig);
chain
.scratch()
.apply(move |scratch| {
scratch.add_action(signed_action, ChainTopOrdering::Strict);
scratch.add_entry(entry_hashed, ChainTopOrdering::Strict);
})
.unwrap();
}
let records = chain.query(ChainQueryFilter::default()).await.unwrap();
let full_ranges = [
ChainQueryFilterRange::Unbounded,
ChainQueryFilterRange::ActionSeqRange(0, 4),
ChainQueryFilterRange::ActionHashRange(
records[0].action_address().clone(),
records[4].action_address().clone(),
),
ChainQueryFilterRange::ActionHashTerminated(records[4].action_address().clone(), 4),
];
let cases = [
((None, None, vec![], false), 5),
((None, None, vec![], true), 5),
((Some(vec![ActionType::Dna]), None, vec![], false), 1),
((None, Some(vec![EntryType::AgentPubKey]), vec![], false), 1),
((None, Some(vec![EntryType::AgentPubKey]), vec![], true), 1),
((Some(vec![ActionType::Create]), None, vec![], false), 2),
((Some(vec![ActionType::Create]), None, vec![], true), 2),
(
(
Some(vec![ActionType::Create]),
Some(vec![EntryType::AgentPubKey]),
vec![],
false,
),
1,
),
(
(
Some(vec![ActionType::Create]),
Some(vec![EntryType::AgentPubKey]),
vec![records[2].action().entry_hash().unwrap().clone()],
true,
),
1,
),
(
(
Some(vec![ActionType::Create, ActionType::Dna]),
None,
vec![],
true,
),
3,
),
(
(
None,
Some(vec![EntryType::AgentPubKey, app_entry_type]),
vec![],
true,
),
3,
),
];
for ((action_type, entry_type, entry_hashes, include_entries), num_expected) in cases {
let entry_hashes = if entry_hashes.is_empty() {
None
} else {
Some(entry_hashes.into_iter().collect())
};
for sequence_range in full_ranges.clone() {
let query = ChainQueryFilter {
sequence_range: sequence_range.clone(),
action_type: action_type.clone(),
entry_type: entry_type.clone(),
entry_hashes: entry_hashes.clone(),
include_entries,
order_descending: false,
};
let queried = chain.query(query.clone()).await.unwrap();
let actual = queried.len();
assert!(queried.iter().all(|e| e.action().author() == &alice));
assert_eq!(
num_expected, actual,
"Expected {num_expected} items but got {actual} with filter {query:?}"
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn source_chain_query_private_entry_redacted_under_public_only() {
let TestCase {
mut chain,
agent_key: alice,
keystore,
..
} = TestCase::new().await;
let private_entry_type = EntryType::App(AppEntryDef {
zome_index: 0.into(),
entry_index: 0.into(),
visibility: EntryVisibility::Private,
});
let public_entry_type = EntryType::App(AppEntryDef {
zome_index: 0.into(),
entry_index: 1.into(),
visibility: EntryVisibility::Public,
});
let chain_top = chain.chain_head_nonempty().unwrap();
let private_entry_hashed = EntryHashed::from_content_sync(Entry::App(fixt!(AppEntryBytes)));
let private_create_action = Action {
header: ActionHeader {
author: alice.clone(),
timestamp: Timestamp::now(),
action_seq: chain_top.seq + 1,
prev_action: Some(chain_top.action.as_hash().clone()),
},
data: ActionData::Create(CreateData {
entry_type: private_entry_type.clone(),
entry_hash: private_entry_hashed.hash.clone(),
}),
};
let sig = alice.sign(&keystore, &private_create_action).await.unwrap();
let private_sah = SignedActionHashed::with_presigned(
HoloHashed::from_content_sync(private_create_action),
sig,
);
let private_action_hash = private_sah.as_hash().clone();
chain
.scratch()
.apply({
let private_sah = private_sah.clone();
move |scratch| {
scratch.add_action(private_sah, ChainTopOrdering::Strict);
scratch.add_entry(private_entry_hashed, ChainTopOrdering::Strict);
}
})
.unwrap();
chain.flush(vec![DhtArc::Empty]).await.unwrap();
let chain_top = chain.chain_head_nonempty().unwrap();
let public_entry_hashed = EntryHashed::from_content_sync(Entry::App(fixt!(AppEntryBytes)));
let public_create_action = Action {
header: ActionHeader {
author: alice.clone(),
timestamp: Timestamp::now(),
action_seq: chain_top.seq + 1,
prev_action: Some(chain_top.action.as_hash().clone()),
},
data: ActionData::Create(CreateData {
entry_type: public_entry_type.clone(),
entry_hash: public_entry_hashed.hash.clone(),
}),
};
let sig = alice.sign(&keystore, &public_create_action).await.unwrap();
let public_sah = SignedActionHashed::with_presigned(
HoloHashed::from_content_sync(public_create_action),
sig,
);
let scratch_action_hash = public_sah.as_hash().clone();
chain
.scratch()
.apply({
let public_sah = public_sah.clone();
move |scratch| {
scratch.add_action(public_sah, ChainTopOrdering::Strict);
scratch.add_entry(public_entry_hashed, ChainTopOrdering::Strict);
}
})
.unwrap();
let q = ChainQueryFilter::default().include_entries(true);
let records = chain.query(q.clone()).await.unwrap();
let committed_private = records
.iter()
.find(|r| r.action_address() == &private_action_hash)
.expect("committed private record present");
assert!(
matches!(committed_private.entry(), RecordEntry::Present(_)),
"private entry should be present without public_only"
);
assert!(
records
.iter()
.any(|r| r.action_address() == &scratch_action_hash),
"uncommitted scratch record should be visible"
);
chain.public_only();
let records = chain.query(q).await.unwrap();
let committed_private = records
.iter()
.find(|r| r.action_address() == &private_action_hash)
.expect("committed private action still present under public_only");
assert!(
matches!(committed_private.entry(), RecordEntry::Hidden),
"private entry should be redacted (Hidden) under public_only, got {:?}",
committed_private.entry()
);
let scratch_record = records
.iter()
.find(|r| r.action_address() == &scratch_action_hash)
.expect("scratch record still visible under public_only");
assert!(
matches!(scratch_record.entry(), RecordEntry::Present(_)),
"scratch entry (own data) should remain present under public_only"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn source_chain_query_ordering() {
let TestCase { chain, .. } = TestCase::new().await;
let asc = chain.query(ChainQueryFilter::default()).await.unwrap();
let desc = chain
.query(ChainQueryFilter::default().descending())
.await
.unwrap();
assert_eq!(asc.len(), 3);
assert_ne!(asc, desc);
let mut desc_sorted = desc;
desc_sorted.sort_by_key(|r| r.action().action_seq());
assert_eq!(asc, desc_sorted);
}
#[tokio::test(flavor = "multi_thread")]
async fn init_zomes_complete() {
let TestCase { chain, .. } = TestCase::new().await;
let zomes_initialized = chain.zomes_initialized().await.unwrap();
assert!(!zomes_initialized);
let result = chain
.put(
ActionData::InitZomesComplete(InitZomesCompleteData {}),
None,
ChainTopOrdering::Strict,
)
.await;
assert!(result.is_ok());
chain.flush(vec![DhtArc::Empty]).await.unwrap();
let zomes_initialized = chain.zomes_initialized().await.unwrap();
assert!(zomes_initialized);
}
#[tokio::test(flavor = "multi_thread")]
async fn flush_writes_warrants_to_dht_store() {
let TestCase {
chain,
agent_key,
dht_store,
keystore,
..
} = TestCase::new().await;
let warrantee = fixt!(AgentPubKey);
let actual_warrants = dht_store
.as_read()
.warrants_by_author(agent_key.clone())
.await
.unwrap();
assert_eq!(actual_warrants.len(), 0);
let signed_warrant = create_signed_warrant(&agent_key, &warrantee, &keystore).await;
chain
.scratch
.apply(|scratch| {
scratch.add_warrant(signed_warrant.clone());
})
.unwrap();
let (actions, warrant_count) = chain.flush(vec![]).await.unwrap();
assert!(actions.is_empty());
assert_eq!(warrant_count, 1);
let actual_warrants = dht_store
.as_read()
.warrants_by_author(agent_key.clone())
.await
.unwrap();
assert_eq!(actual_warrants, vec![WarrantOp::from(signed_warrant)]);
}
#[tokio::test(flavor = "multi_thread")]
async fn duplicate_warrants_are_not_inserted_during_flush() {
holochain_trace::test_run();
let TestCase {
chain,
agent_key,
dht_store,
keystore,
..
} = TestCase::new().await;
let warrantee = fixt!(AgentPubKey);
let signed_warrant = create_signed_warrant(&agent_key, &warrantee, &keystore).await;
chain
.scratch
.apply(|scratch| {
scratch.add_warrant(signed_warrant.clone());
})
.unwrap();
let (actions, warrant_count) = chain.flush(vec![]).await.unwrap();
assert!(actions.is_empty());
assert_eq!(warrant_count, 1);
let actual_warrants = dht_store
.as_read()
.warrants_by_author(agent_key.clone())
.await
.unwrap();
assert_eq!(
actual_warrants,
vec![WarrantOp::from(signed_warrant.clone())]
);
chain
.scratch
.apply(|scratch| {
scratch.add_warrant(signed_warrant.clone());
})
.unwrap();
let (actions, warrant_count) = chain.flush(vec![]).await.unwrap();
assert!(actions.is_empty());
assert_eq!(warrant_count, 1);
let actual_warrants = dht_store
.as_read()
.warrants_by_author(agent_key.clone())
.await
.unwrap();
assert_eq!(actual_warrants, vec![WarrantOp::from(signed_warrant)]);
}
#[tokio::test(flavor = "multi_thread")]
async fn counterfeit_warrants_are_not_inserted_during_flush() {
let TestCase {
chain,
agent_key,
dht_store,
..
} = TestCase::new().await;
let warrantee = fixt!(AgentPubKey);
let warrant = Warrant::new(
WarrantProof::ChainIntegrity(ChainIntegrityWarrant::InvalidChainOp {
action_author: warrantee.clone(),
action: (fixt!(ActionHash), fixt!(Signature)),
chain_op_type: ChainOpType::AgentActivity,
reason: "invalid chain op".into(),
}),
agent_key.clone(),
Timestamp::now(),
warrantee.clone(),
);
let signed_warrant = SignedWarrant::new(warrant, fixt!(Signature));
chain
.scratch
.apply(|scratch| {
scratch.add_warrant(signed_warrant.clone());
})
.unwrap();
let (actions, warrant_count) = chain.flush(vec![]).await.unwrap();
assert!(actions.is_empty());
assert_eq!(warrant_count, 0);
let actual_warrants = dht_store
.as_read()
.warrants_by_author(agent_key.clone())
.await
.unwrap();
assert!(actual_warrants.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn flush_countersigning_op_sets_withhold_publish() {
use holochain_zome_types::prelude::{
CounterSigningAgentState, CounterSigningSessionData, CounterSigningSessionTimes,
PreflightRequest,
};
use std::time::Duration;
let TestCase {
chain,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let bob = keystore.new_sign_keypair_random().await.unwrap();
let app_entry_hash = fixt!(EntryHash);
let app_entry_type = EntryType::App(AppEntryDef::new(
EntryDefIndex(0),
0.into(),
EntryVisibility::Public,
));
let start = Timestamp::now();
let end = (start + Duration::from_secs(60)).unwrap();
let session_times = CounterSigningSessionTimes::try_new(start, end).unwrap();
let preflight_request = PreflightRequest::try_new(
app_entry_hash,
vec![(alice.clone(), vec![]), (bob.clone(), vec![])],
vec![],
0,
false,
session_times,
ActionBase::Create(CreateBase::new(app_entry_type.clone())),
PreflightBytes(vec![]),
)
.unwrap();
let alice_agent_state = chain
.accept_countersigning_preflight_request(preflight_request.clone(), 0)
.await
.unwrap();
let bob_agent_state = CounterSigningAgentState::new(1, fixt!(ActionHash), 2);
let session_data = CounterSigningSessionData::try_new(
preflight_request,
vec![
(alice_agent_state, fixt!(Signature)),
(bob_agent_state, fixt!(Signature)),
],
vec![],
)
.unwrap();
let entry = Entry::CounterSign(Box::new(session_data), fixt!(AppEntryBytes));
chain
.put_countersigned(entry, ChainTopOrdering::Strict)
.await
.unwrap();
chain.flush(vec![DhtArc::Empty]).await.unwrap();
let withheld_count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM ChainOpPublish WHERE withhold_publish = 1")
.fetch_one(dht_store.db().pool())
.await
.unwrap();
assert!(
withheld_count > 0,
"expected at least one ChainOpPublish row with withhold_publish=1 \
after countersigning flush, got 0"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn chain_head_read_from_store() -> SourceChainResult<()> {
let TestCase {
chain,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let storage_arcs = vec![DhtArc::Empty];
chain
.put(
ActionData::CloseChain(CloseChainData { new_target: None }),
None,
ChainTopOrdering::Strict,
)
.await?;
let (flushed_actions, _) = chain.flush(storage_arcs).await?;
let expected_head = flushed_actions
.last()
.expect("flush must return at least one action")
.as_hash()
.clone();
let chain2 = SourceChain::new(dht_store.clone(), keystore, alice).await?;
assert_eq!(
chain2.persisted_head_info().map(|h| h.action),
Some(expected_head),
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn flush_as_at_detects_head_moved_against_store() -> SourceChainResult<()> {
let TestCase {
chain: chain_1,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let chain_2 = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let close_chain = ActionData::CloseChain(CloseChainData { new_target: None });
chain_1
.put(close_chain.clone(), None, ChainTopOrdering::Strict)
.await?;
chain_2
.put(close_chain, None, ChainTopOrdering::Strict)
.await?;
let (flushed, _) = chain_1.flush(vec![DhtArc::Empty]).await?;
let flushed_head = flushed
.last()
.expect("flush returns at least one action")
.as_hash()
.clone();
let store_head = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("store head present after flush");
assert_eq!(store_head.action, flushed_head);
assert_matches!(
chain_2.flush(vec![DhtArc::Empty]).await,
Err(SourceChainError::HeadMoved(_, _, _, _))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_strict_flushes_do_not_fork_chain() -> SourceChainResult<()> {
let TestCase {
chain: _chain,
agent_key: alice,
dht_store,
keystore,
..
} = TestCase::new().await;
let pre_seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("genesis chain head present")
.seq;
let chain_a = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let chain_b = SourceChain::new(dht_store.clone(), keystore.clone(), alice.clone()).await?;
let close_chain = ActionData::CloseChain(CloseChainData { new_target: None });
chain_a
.put(close_chain.clone(), None, ChainTopOrdering::Strict)
.await?;
chain_b
.put(close_chain, None, ChainTopOrdering::Strict)
.await?;
let arcs_a = vec![DhtArc::Empty];
let arcs_b = arcs_a.clone();
let task_a = tokio::spawn(async move { chain_a.flush(arcs_a).await });
let task_b = tokio::spawn(async move { chain_b.flush(arcs_b).await });
let (res_a, res_b) = tokio::join!(task_a, task_b);
let res_a = res_a.expect("flush task a did not panic");
let res_b = res_b.expect("flush task b did not panic");
let oks = [&res_a, &res_b].iter().filter(|r| r.is_ok()).count();
assert_eq!(
oks, 1,
"exactly one flush must commit; a={res_a:?}, b={res_b:?}"
);
let loser = if res_a.is_err() { &res_a } else { &res_b };
assert_matches!(loser, Err(SourceChainError::HeadMoved(_, _, _, _)));
let new_seq = dht_store
.as_read()
.chain_head_for_author(&alice)
.await?
.expect("chain head present after flush")
.seq;
assert_eq!(
new_seq,
pre_seq + 1,
"store head must advance by exactly one, not fork"
);
Ok(())
}
struct TestCase {
chain: SourceChain,
agent_key: AgentPubKey,
dht_store: DhtStore,
keystore: MetaLairClient,
}
impl TestCase {
async fn new() -> Self {
let keystore = test_keystore();
let dna_hash = fixt!(DnaHash);
let dht_store = crate::test_utils::test_dht_store(dna_hash.clone()).await;
let agent_key = keystore.new_sign_keypair_random().await.unwrap();
genesis(
dht_store.clone(),
keystore.clone(),
dna_hash,
agent_key.clone(),
None,
)
.await
.unwrap();
let chain = SourceChain::new(dht_store.clone(), keystore.clone(), agent_key.clone())
.await
.unwrap();
Self {
chain,
agent_key,
dht_store,
keystore,
}
}
}
async fn create_signed_warrant(
author: &AgentPubKey,
warrantee: &AgentPubKey,
keystore: &MetaLairClient,
) -> SignedWarrant {
let warrant = Warrant::new(
WarrantProof::ChainIntegrity(ChainIntegrityWarrant::InvalidChainOp {
action_author: warrantee.clone(),
action: (fixt!(ActionHash), fixt!(Signature)),
chain_op_type: ChainOpType::AgentActivity,
reason: "invalid chain op".into(),
}),
author.clone(),
Timestamp::now(),
warrantee.clone(),
);
SignedWarrant::new(
warrant.clone(),
author.sign(keystore, warrant).await.unwrap(),
)
}
}