diff --git a/CHANGELOG-UNRELEASED.md b/CHANGELOG-UNRELEASED.md index 2f730af61f..68ef5c410a 100644 --- a/CHANGELOG-UNRELEASED.md +++ b/CHANGELOG-UNRELEASED.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +* Adds publishing of headers again after rollback. Header publishing is now its own action rather than part of the `Publish` action that plays nicely with the testing framework. It also adds header entries to the author list so they are gossiped properly. [#1640](https://github.com/holochain/holochain-rust/pull/1640). + ### Changed ### Deprecated diff --git a/app_spec/test/files/links.js b/app_spec/test/files/links.js index c7a455bc10..7fdbba0bfb 100644 --- a/app_spec/test/files/links.js +++ b/app_spec/test/files/links.js @@ -43,23 +43,23 @@ module.exports = scenario => { //bob expects zero links t.ok(bob_agent_posts_expect_empty.Ok) - t.equal(bob_agent_posts_expect_empty.Ok.links.length, 0); - //alice expects zero alice + t.equal(bob_agent_posts_expect_empty.Ok.links.length, 0); // #!# fails with expected: 0 actual: 2 + //alice expects zero links t.ok(alice_agent_posts_expect_empty.Ok) t.equal(alice_agent_posts_expect_empty.Ok.links.length, 0); - //different chain hash up to this point so we should be able to create a link with the same data await alice.app.callSync("simple", "create_link",{ "base":alice.app.agentId, "target": "Posty" }) - //get alice posts - const alice_posts_not_empty = await bob.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" }) + //get posts as Alice and as Bob + const alice_posts_not_empty = await alice.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" }) + const bob_posts_not_empty = await bob.app.call("simple", "get_my_links",{ "base": alice.app.agentId,"status_request" : "Live" }) //expect 1 post t.ok(alice_posts_not_empty.Ok) t.equal(alice_posts_not_empty.Ok.links.length, 1); - - + t.ok(bob_posts_not_empty.Ok) + t.equal(bob_posts_not_empty.Ok.links.length, 1); //#!# fails with expected: 1 actual: 2 }) diff --git a/core/src/action.rs b/core/src/action.rs index 881d81abe6..2bf6e67e99 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -140,6 +140,10 @@ pub enum Action { /// (only publish for AppEntryType, publish and publish_meta for links etc) Publish(Address), + /// Publish to the network the header entry for the entry at the given address. + /// Note that the given address is that of the entry NOT the address of the header itself + PublishHeaderEntry(Address), + ///Performs a Network Query Action based on the key and payload, used for links and Entries Query((QueryKey, QueryPayload)), diff --git a/core/src/agent/state.rs b/core/src/agent/state.rs index ace1ddbafb..49b36074ad 100644 --- a/core/src/agent/state.rs +++ b/core/src/agent/state.rs @@ -421,7 +421,7 @@ pub mod tests { let header = create_new_chain_header( &test_entry(), &agent_state, - &StateWrapper::from(state.clone()), + &StateWrapper::from(state), &None, &vec![], ) diff --git a/core/src/network/actions/mod.rs b/core/src/network/actions/mod.rs index a82d33acf3..56210ef581 100644 --- a/core/src/network/actions/mod.rs +++ b/core/src/network/actions/mod.rs @@ -4,6 +4,7 @@ pub mod get_validation_package; pub mod initialize_network; pub mod publish; pub mod shutdown; +pub mod publish_header_entry; use holochain_core_types::error::HcResult; use holochain_persistence_api::cas::content::Address; @@ -11,5 +12,6 @@ use holochain_persistence_api::cas::content::Address; #[derive(Clone, Debug)] pub enum ActionResponse { Publish(HcResult
), + PublishHeaderEntry(HcResult
), Respond(HcResult<()>), } diff --git a/core/src/network/actions/publish_header_entry.rs b/core/src/network/actions/publish_header_entry.rs new file mode 100644 index 0000000000..388f826714 --- /dev/null +++ b/core/src/network/actions/publish_header_entry.rs @@ -0,0 +1,54 @@ +use crate::{ + action::{Action, ActionWrapper}, + context::Context, + instance::dispatch_action, + network::actions::ActionResponse, +}; +use futures::{future::Future, task::Poll}; +use holochain_core_types::error::HcResult; +use holochain_persistence_api::cas::content::Address; +use std::{pin::Pin, sync::Arc}; + +/// Publish Header Entry Action Creator +/// Returns a future that resolves to an ActionResponse. +pub async fn publish_header_entry(address: Address, context: &Arc) -> HcResult
{ + let action_wrapper = ActionWrapper::new(Action::PublishHeaderEntry(address)); + dispatch_action(context.action_channel(), action_wrapper.clone()); + await!(PublishHeaderEntryFuture { + context: context.clone(), + action: action_wrapper, + }) +} + +/// PublishFuture resolves to ActionResponse +/// Tracks the state for a response to its ActionWrapper +pub struct PublishHeaderEntryFuture { + context: Arc, + action: ActionWrapper, +} + +impl Future for PublishHeaderEntryFuture { + type Output = HcResult
; + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context) -> Poll { + if let Some(err) = self.context.action_channel_error("PublishHeaderEntryFuture") { + return Poll::Ready(Err(err)); + } + let state = self.context.state().unwrap().network(); + if let Err(error) = state.initialized() { + return Poll::Ready(Err(error)); + } + // + // TODO: connect the waker to state updates for performance reasons + // See: https://github.com/holochain/holochain-rust/issues/314 + // + cx.waker().clone().wake(); + match state.actions().get(&self.action) { + Some(ActionResponse::PublishHeaderEntry(result)) => match result { + Ok(address) => Poll::Ready(Ok(address.to_owned())), + Err(error) => Poll::Ready(Err(error.clone())), + }, + _ => Poll::Pending, + } + } +} diff --git a/core/src/network/handler/lists.rs b/core/src/network/handler/lists.rs index 2859371bb5..972a24e837 100644 --- a/core/src/network/handler/lists.rs +++ b/core/src/network/handler/lists.rs @@ -5,11 +5,16 @@ use crate::{ instance::dispatch_action, network::handler::{get_content_aspect, get_meta_aspects}, }; -use holochain_core_types::error::HcResult; +use holochain_core_types::{ + error::HcResult, + entry::Entry, +}; use holochain_persistence_api::cas::content::{Address, AddressableContent}; use lib3h_protocol::data_types::{EntryListData, GetListData}; use snowflake::ProcessUniqueId; use std::{collections::HashMap, sync::Arc, thread}; +use crate::network::entry_aspect::EntryAspect; +use crate::agent::state::create_new_chain_header; pub fn handle_get_authoring_list(get_list_data: GetListData, context: Arc) { thread::Builder::new() @@ -20,11 +25,32 @@ pub fn handle_get_authoring_list(get_list_data: GetListData, context: Arc) -> Vec
{ - let chain = context.state().unwrap().agent().chain_store(); - let top_header = context.state().unwrap().agent().top_chain_header(); + let chain = context.state().unwrap().agent().iter_chain(); chain - .iter(&top_header) .filter(|ref chain_header| chain_header.entry_type().can_publish(&context)) .map(|chain_header| chain_header.entry_address().clone()) .collect() } +fn get_all_chain_header_entries(context: Arc) -> Vec { + let chain = context.state().unwrap().agent().iter_chain(); + chain + .map(|chain_header| Entry::ChainHeader(chain_header)) + .collect() +} + fn get_all_aspect_addresses(entry: &Address, context: Arc) -> HcResult> { let mut address_list: Vec
= get_meta_aspects(entry, context.clone())? .iter() @@ -90,3 +121,67 @@ pub fn handle_get_gossip_list(get_list_data: GetListData, context: Arc) }) .expect("Could not spawn thread for creating of gossip list"); } + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::workflows::author_entry::author_entry; + use crate::nucleus::actions::tests::*; + use holochain_core_types::{ + entry::{Entry, test_entry_with_value}, + }; + use holochain_persistence_api::cas::content::AddressableContent; + use std::{thread, time}; + + #[test] + fn test_can_get_chain_header_list() { + let mut dna = test_dna(); + dna.uuid = "test_can_get_chain_header_list".to_string(); + let (_instance, context) = instance_by_name("jill", dna, None); + + context + .block_on(author_entry( + &test_entry_with_value("{\"stuff\":\"test entry value\"}"), + None, + &context, + &vec![], + )) + .unwrap() + .address(); + + thread::sleep(time::Duration::from_millis(500)); + + let chain = context.state().unwrap().agent().iter_chain(); + let header_entries: Vec = chain.map(|header| Entry::ChainHeader(header)).collect(); + + assert_eq!( + get_all_chain_header_entries(context), + header_entries, + ) + + } + + #[test] + fn test_can_get_all_aspect_addr_for_headers() { + let mut dna = test_dna(); + dna.uuid = "test_can_get_chain_header_list".to_string(); + let (_instance, context) = instance_by_name("jill", dna, None); + + context + .block_on(author_entry( + &test_entry_with_value("{\"stuff\":\"test entry value\"}"), + None, + &context, + &vec![], + )) + .unwrap() + .address(); + + thread::sleep(time::Duration::from_millis(500)); + + assert!(get_all_chain_header_entries(context.clone()).iter().all(|chain_header| { + get_all_aspect_addresses(&chain_header.address(), context.clone()).is_ok() + })); + } + +} diff --git a/core/src/network/handler/mod.rs b/core/src/network/handler/mod.rs index 6c134d0802..98caffbd39 100644 --- a/core/src/network/handler/mod.rs +++ b/core/src/network/handler/mod.rs @@ -18,7 +18,6 @@ use crate::{ store::*, }, }, - nucleus, workflows::get_entry_result::get_entry_with_meta_workflow, }; use boolinator::*; @@ -31,6 +30,8 @@ use lib3h_protocol::{ protocol_server::Lib3hServerProtocol, }; use std::{convert::TryFrom, sync::Arc}; +use crate::nucleus::actions::get_entry::get_entry_from_cas; +use crate::network::entry_with_header::EntryWithHeader; // FIXME: Temporary hack to ignore messages incorrectly sent to us by the networking // module that aren't really meant for us @@ -252,35 +253,67 @@ fn get_content_aspect( entry_address: &Address, context: Arc, ) -> Result { - let entry_with_meta = - nucleus::actions::get_entry::get_entry_with_meta(&context, entry_address.clone())? - .ok_or(HolochainError::EntryNotFoundLocally)?; + let state = context.state() + .ok_or_else(|| { + HolochainError::InitializationFailed( + String::from("In get_content_aspect: no state found") + ) + })?; + + // Optimistically look for entry in chain... + let maybe_chain_header = state.agent() + .iter_chain() + .find(|ref chain_header| chain_header.entry_address() == entry_address); + + // If we have found a header for the requested entry in the chain... + let maybe_entry_with_header = if let Some(header) = maybe_chain_header { + // ... we can just get the content from the chain CAS + Some(EntryWithHeader { + entry: get_entry_from_cas(&state.agent().chain_store().content_storage(), header.entry_address())? + .expect("Could not find entry in chain CAS, but header is chain"), + header + }) + } else { + // ... but if we didn't author that entry, let's see if we have it in the DHT cas: + if let Some(entry) = get_entry_from_cas(&state.dht().content_storage(), entry_address)? { + // If we have it in the DHT cas that's good, + // but then we have to get the header like this: + let headers = context + .state() + .expect("Could not get state for handle_fetch_entry") + .get_headers(entry_address.clone()) + .map_err(|error| { + let err_message = format!( + "net/fetch/get_content_aspect: Error trying to get headers {:?}", + error + ); + log_error!(context, "{}", err_message.clone()); + HolochainError::ErrorGeneric(err_message) + })?; + if headers.len() > 0 { + // TODO: this is just taking the first header.. + // We should actually transform all headers into EntryAspect::Headers and just the first one + // into an EntryAspect content (What about ordering? Using the headers timestamp?) + Some(EntryWithHeader{entry, header: headers[0].clone()}) + } else { + None + } + } else { + None + } + }; + + let entry_with_header = maybe_entry_with_header.ok_or(HolochainError::EntryNotFoundLocally)?; - let _ = entry_with_meta + let _ = entry_with_header .entry .entry_type() .can_publish(&context) .ok_or(HolochainError::EntryIsPrivate)?; - let headers = context - .state() - .expect("Could not get state for handle_fetch_entry") - .get_headers(entry_address.clone()) - .map_err(|error| { - let err_message = format!( - "net/fetch/get_content_aspect: Error trying to get headers {:?}", - error - ); - log_error!(context, "{}", err_message.clone()); - HolochainError::ErrorGeneric(err_message) - })?; - - // TODO: this is just taking the first header.. - // We should actually transform all headers into EntryAspect::Headers and just the first one - // into an EntryAspect content (What about ordering? Using the headers timestamp?) Ok(EntryAspect::Content( - entry_with_meta.entry, - headers[0].clone(), + entry_with_header.entry, + entry_with_header.header, )) } diff --git a/core/src/network/reducers/mod.rs b/core/src/network/reducers/mod.rs index 4b17cc5581..d51605518f 100644 --- a/core/src/network/reducers/mod.rs +++ b/core/src/network/reducers/mod.rs @@ -5,6 +5,7 @@ pub mod handle_get_result; pub mod handle_get_validation_package; pub mod init; pub mod publish; +pub mod publish_header_entry; pub mod resolve_direct_connection; pub mod respond_authoring_list; pub mod respond_fetch; @@ -25,6 +26,7 @@ use crate::{ handle_get_validation_package::reduce_handle_get_validation_package, init::reduce_init, publish::reduce_publish, + publish_header_entry::reduce_publish_header_entry, resolve_direct_connection::reduce_resolve_direct_connection, respond_authoring_list::reduce_respond_authoring_list, respond_fetch::reduce_respond_fetch_data, @@ -58,6 +60,7 @@ fn resolve_reducer(action_wrapper: &ActionWrapper) -> Option { Action::HandleGetValidationPackage(_) => Some(reduce_handle_get_validation_package), Action::InitNetwork(_) => Some(reduce_init), Action::Publish(_) => Some(reduce_publish), + Action::PublishHeaderEntry(_) => Some(reduce_publish_header_entry), Action::ResolveDirectConnection(_) => Some(reduce_resolve_direct_connection), Action::RespondAuthoringList(_) => Some(reduce_respond_authoring_list), Action::RespondGossipList(_) => Some(reduce_respond_gossip_list), diff --git a/core/src/network/reducers/publish.rs b/core/src/network/reducers/publish.rs index ee43398292..f1c5f4a7ad 100644 --- a/core/src/network/reducers/publish.rs +++ b/core/src/network/reducers/publish.rs @@ -127,6 +127,7 @@ fn reduce_publish_inner( network_state.initialized()?; let entry_with_header = fetch_entry_with_header(&address, root_state)?; + match entry_with_header.entry.entry_type() { EntryType::AgentId => publish_entry(network_state, &entry_with_header), EntryType::App(_) => publish_entry(network_state, &entry_with_header).and_then(|_| { @@ -156,7 +157,7 @@ fn reduce_publish_inner( } }), _ => Err(HolochainError::NotImplemented( - "reduce_publish_inner".into(), + format!("reduce_publish_inner not implemented for {}", entry_with_header.entry.entry_type()), )), } } diff --git a/core/src/network/reducers/publish_header_entry.rs b/core/src/network/reducers/publish_header_entry.rs new file mode 100644 index 0000000000..97132d763c --- /dev/null +++ b/core/src/network/reducers/publish_header_entry.rs @@ -0,0 +1,110 @@ +use crate::{ + action::ActionWrapper, + network::{ + actions::ActionResponse, + entry_aspect::EntryAspect, + entry_with_header::{fetch_entry_with_header}, + reducers::send, + state::NetworkState, + }, + state::State, + agent::state::create_new_chain_header, +}; +use holochain_core_types::{ + entry::{Entry}, + error::HolochainError, + chain_header::ChainHeader, +}; +use lib3h_protocol::{ + data_types::{EntryData, ProvidedEntryData}, + protocol_client::Lib3hClientProtocol, +}; + +use holochain_persistence_api::cas::content::{Address, AddressableContent}; +use crate::state::StateWrapper; + + +/// Send to network a request to publish a header entry alone +/// This is similar to publishing a regular entry but it has its own special dummy header. +fn publish_header( + network_state: &mut NetworkState, + root_state: &State, + chain_header: &ChainHeader, +) -> Result<(), HolochainError> { + let header_entry = Entry::ChainHeader(chain_header.clone()); + let header_entry_header = create_new_chain_header( + &header_entry, + &root_state.agent(), + &StateWrapper::from(root_state.clone()), + &None, + &Vec::new(), + )?; + send( + network_state, + Lib3hClientProtocol::PublishEntry(ProvidedEntryData { + space_address: network_state.dna_address.clone().unwrap(), + provider_agent_id: network_state.agent_id.clone().unwrap().into(), + entry: EntryData { + entry_address: header_entry.address().clone(), + aspect_list: vec![EntryAspect::Content( + header_entry.clone(), + header_entry_header, + ) + .into()], + }, + }), + ) +} + + +fn reduce_publish_header_entry_inner( + network_state: &mut NetworkState, + root_state: &State, + address: &Address, +) -> Result<(), HolochainError> { + network_state.initialized()?; + let entry_with_header = fetch_entry_with_header(&address, root_state)?; + publish_header(network_state, root_state, &entry_with_header.header) +} + +pub fn reduce_publish_header_entry( + network_state: &mut NetworkState, + root_state: &State, + action_wrapper: &ActionWrapper, +) { + let action = action_wrapper.action(); + let address = unwrap_to!(action => crate::action::Action::PublishHeaderEntry); + + let result = reduce_publish_header_entry_inner(network_state, root_state, &address); + network_state.actions.insert( + action_wrapper.clone(), + ActionResponse::PublishHeaderEntry(match result { + Ok(_) => Ok(address.clone()), + Err(e) => Err(HolochainError::ErrorGeneric(e.to_string())), + }), + ); +} + +#[cfg(test)] +mod tests { + + use crate::{ + action::{Action, ActionWrapper}, + instance::tests::test_context, + state::test_store, + }; + use holochain_core_types::entry::test_entry; + use holochain_persistence_api::cas::content::AddressableContent; + + #[test] + pub fn reduce_publish_header_entry_test() { + let context = test_context("alice", None); + let store = test_store(context.clone()); + + let entry = test_entry(); + let action_wrapper = ActionWrapper::new(Action::PublishHeaderEntry(entry.address())); + + store.reduce(action_wrapper); + } + +} diff --git a/core/src/nucleus/actions/build_validation_package.rs b/core/src/nucleus/actions/build_validation_package.rs index 8d5f2fc9e4..6e87cac3fe 100644 --- a/core/src/nucleus/actions/build_validation_package.rs +++ b/core/src/nucleus/actions/build_validation_package.rs @@ -6,6 +6,7 @@ use crate::{ nucleus::ribosome::callback::{ validation_package::get_validation_package_definition, CallbackResult, }, + state::State, }; use futures::{future::Future, task::Poll}; use holochain_core_types::{ @@ -18,6 +19,7 @@ use holochain_core_types::{ }; use snowflake; use std::{convert::TryInto, pin::Pin, sync::Arc, thread, vec::Vec}; +use crate::state::StateWrapper; pub async fn build_validation_package<'a>( entry: &'a Entry, @@ -88,11 +90,11 @@ pub async fn build_validation_package<'a>( // and just used for the validation, I don't see why it would be a problem. // If it was a problem, we would have to make sure that the whole commit process // (including validtion) is atomic. - let state = &context.state()?; + let state = State::new(context.clone()); agent::state::create_new_chain_header( &entry, - &state.agent(), - &*state, + &context.state()?.agent(), + &StateWrapper::from(state), &None, provenances, )? diff --git a/core/src/nucleus/actions/get_entry.rs b/core/src/nucleus/actions/get_entry.rs index 01b48bf816..3f98b20cdb 100644 --- a/core/src/nucleus/actions/get_entry.rs +++ b/core/src/nucleus/actions/get_entry.rs @@ -7,13 +7,12 @@ use holochain_core_types::{ }; use holochain_persistence_api::{ - cas::{content::Address, storage::ContentAddressableStorage}, + cas::{content::{Address, AddressableContent}, storage::ContentAddressableStorage}, eav::IndexFilter, }; use std::{ collections::BTreeSet, - convert::TryInto, str::FromStr, sync::{Arc, RwLock}, }; @@ -22,10 +21,12 @@ pub(crate) fn get_entry_from_cas( storage: &Arc>, address: &Address, ) -> Result, HolochainError> { - let json = (*storage.read().unwrap()).fetch(&address)?; - - let entry: Option = json.and_then(|js| js.try_into().ok()); - Ok(entry) + if let Some(json) = (*storage.read().unwrap()).fetch(&address)? { + let entry = Entry::try_from_content(&json)?; + Ok(Some(entry)) + } else { + Ok(None) // no errors but entry is not in CAS + } } pub fn get_entry_from_agent_chain( diff --git a/core/src/nucleus/ribosome/callback/validation_package.rs b/core/src/nucleus/ribosome/callback/validation_package.rs index 2a85137d58..68bf2cae92 100644 --- a/core/src/nucleus/ribosome/callback/validation_package.rs +++ b/core/src/nucleus/ribosome/callback/validation_package.rs @@ -109,6 +109,7 @@ pub fn get_validation_package_definition( EntryType::Deletion => JsonString::from(ValidationPackageDefinition::ChainFull), EntryType::CapTokenGrant => JsonString::from(ValidationPackageDefinition::Entry), EntryType::AgentId => JsonString::from(ValidationPackageDefinition::Entry), + EntryType::ChainHeader => JsonString::from(ValidationPackageDefinition::Entry), _ => Err(HolochainError::NotImplemented( "get_validation_package_definition/3".into(), ))?, diff --git a/core/src/nucleus/validation/mod.rs b/core/src/nucleus/validation/mod.rs index c9a6f2ad17..ba15fa783e 100644 --- a/core/src/nucleus/validation/mod.rs +++ b/core/src/nucleus/validation/mod.rs @@ -123,6 +123,9 @@ pub async fn validate_entry( context, )), + // chain headers always pass for now. In future this should check that the entry is valid + EntryType::ChainHeader => Ok(()), + _ => Err(ValidationError::NotImplemented), } } diff --git a/core/src/workflows/application.rs b/core/src/workflows/application.rs index 3b346fe835..676301b278 100644 --- a/core/src/workflows/application.rs +++ b/core/src/workflows/application.rs @@ -1,14 +1,19 @@ use crate::{ context::{get_dna_and_agent, Context}, instance::Instance, - network::actions::initialize_network::initialize_network, + network::actions::{ + publish_header_entry::publish_header_entry, + publish::publish, + initialize_network::initialize_network, + }, nucleus::actions::{call_init::call_init, initialize::initialize_chain}, }; use holochain_core_types::{ dna::Dna, error::{HcResult, HolochainError}, + entry::Entry, }; - +use holochain_persistence_api::cas::content::AddressableContent; use std::sync::Arc; pub async fn initialize( @@ -18,15 +23,35 @@ pub async fn initialize( ) -> HcResult> { let instance_context = instance.initialize_context(context.clone()); let dna = dna.ok_or(HolochainError::DnaMissing)?; - if let Err(err) = await!(get_dna_and_agent(&instance_context)) { - log_warn!(context, - "dna/initialize: Couldn't get DNA and agent from chain: {:?}", - err - ); - log_info!(context, "dna/initialize: Initializing new chain from given DNA..."); - await!(initialize_chain(dna.clone(), &instance_context))?; - } + + // 2. Initialize the local chain if not already + let first_initialization = match await!(get_dna_and_agent(&instance_context)) { + Ok(_) => false, + Err(err) => { + log_debug!(context, + "dna/initialize: No DNA and agent in chain so assuming uninitialized: {:?}", + err + ); + await!(initialize_chain(dna.clone(), &instance_context))?; + log_debug!(context, "dna/initialize: Initializing new chain from given DNA..."); + true + } + }; + + // 3. Initialize the network await!(initialize_network(&instance_context))?; - await!(call_init(dna, &instance_context))?; + + if first_initialization { + // 4. (first initialization only) Publish the agent entry and headers of the agent and DNA entries. + await!(publish(context.agent_id.address(), &context))?; + + let dna_entry = Entry::Dna(Box::new(dna.clone())); + await!(publish_header_entry(dna_entry.address(), &context))?; + let agent_id_entry = Entry::AgentId(context.agent_id.clone()); + await!(publish_header_entry(agent_id_entry.address(), &context))?; + + // 5. (first initialization only) Call the init callbacks in the zomes + await!(call_init(dna, &instance_context))?; + } Ok(instance_context) } diff --git a/core/src/workflows/author_entry.rs b/core/src/workflows/author_entry.rs index c5bc8e75f6..a2ff9afbf4 100644 --- a/core/src/workflows/author_entry.rs +++ b/core/src/workflows/author_entry.rs @@ -1,11 +1,14 @@ use crate::{ agent::actions::commit::commit_entry, context::Context, - entry::CanPublish, - network::actions::publish::publish, + network::actions::{ + publish::publish, + publish_header_entry::publish_header_entry, + }, nucleus::{ actions::build_validation_package::build_validation_package, validation::validate_entry, }, + entry::CanPublish, }; use holochain_core_types::{ @@ -95,15 +98,25 @@ pub async fn author_entry<'a>( address ); } + + // 5. Publish the header for all types (including private entries) + log_debug!(context, "debug/workflow/authoring_entry/{}: publishing header...", address); + await!(publish_header_entry(entry.address(), &context))?; + log_debug!(context, "debug/workflow/authoring_entry/{}: header published!", address); + Ok(CommitEntryResult::new(addr)) } #[cfg(test)] pub mod tests { use super::author_entry; + use crate::nucleus::actions::get_entry::get_entry_from_dht; use crate::nucleus::actions::tests::*; - use holochain_core_types::entry::test_entry_with_value; - use holochain_json_api::json::JsonString; + use holochain_core_types::{ + entry::{test_entry_with_value, Entry}, + chain_header::ChainHeader, + }; + use holochain_persistence_api::cas::content::AddressableContent; use std::{thread, time}; #[test] @@ -126,31 +139,153 @@ pub mod tests { .address(); thread::sleep(time::Duration::from_millis(500)); - let mut json: Option = None; + let mut entry: Option = None; let mut tries = 0; - while json.is_none() && tries < 120 { + while entry.is_none() && tries < 120 { tries = tries + 1; { - let state = &context2.state().unwrap(); - json = state - .dht() - .content_storage() - .read() - .unwrap() - .fetch(&entry_address) - .expect("could not fetch from CAS"); + entry = get_entry_from_dht(&context2, &entry_address).expect("Could not retrieve entry from DHT"); } - println!("Try {}: {:?}", tries, json); - if json.is_none() { + println!("Try {}: {:?}", tries, entry); + if entry.is_none() { thread::sleep(time::Duration::from_millis(1000)); } } + assert_eq!( + entry, + Some(test_entry_with_value("{\"stuff\":\"test entry value\"}")) + ); + } + + #[test] + /// test that the header of an entry can be retrieved directly by its hash by another agent connected + /// via the in-memory network + fn test_commit_with_dht_publish_header_is_published() { + let mut dna = test_dna(); + dna.uuid = "test_commit_with_dht_publish_header_is_published".to_string(); + let netname = Some("test_commit_with_dht_publish_header_is_published, the network"); + let (_instance1, context1) = instance_by_name("jill", dna.clone(), netname); + let (_instance2, context2) = instance_by_name("jack", dna, netname); + + let entry_address = context1 + .block_on(author_entry( + &test_entry_with_value("{\"stuff\":\"test entry value\"}"), + None, + &context1, + &vec![], + )) + .unwrap() + .address(); + + thread::sleep(time::Duration::from_millis(500)); + + // get the header from the top of Jill's chain + let state = &context1.state().unwrap(); + let header = state.get_headers(entry_address) + .expect("Could not retrieve headers from authors chain") + .into_iter() + .next() + .expect("No headers were found for this entry in the authors chain"); + let header_entry = Entry::ChainHeader(header); - let x: String = json.unwrap().to_string(); + // try and load it by its address as Jack. This means it has been communicated over the mock network + let mut entry: Option = None; + let mut tries = 0; + while entry.is_none() && tries < 10 { + tries = tries + 1; + { + entry = get_entry_from_dht(&context2, &header_entry.address()).expect("Could not retrieve entry from DHT"); + } + println!("Try {}: {:?}", tries, entry); + if entry.is_none() { + thread::sleep(time::Duration::from_millis(1000)); + } + } assert_eq!( - x, - "{\"App\":[\"testEntryType\",\"{\\\"stuff\\\":\\\"test entry value\\\"}\"]}" - .to_string(), + entry, + Some(header_entry), ); } -} + + + #[test] + /// test that all headers are published so an agents local chain can be reconstructed by another agent + fn test_reconstruct_chain_via_published_headers() { + let mut dna = test_dna(); + dna.uuid = "test_reconstruct_chain_via_published_headers".to_string(); + let netname = Some("test_reconstruct_chain_via_published_headers, the network"); + // the ordering of these is important. Jack will get Jills DNA and AgentId headers but not visa-versa + let (_instance2, context2) = instance_by_name("jack", dna.clone(), netname); + let (_instance1, context1) = instance_by_name("jill", dna.clone(), netname); + + // Jill publishes an entry + context1 + .block_on(author_entry( + &test_entry_with_value("{\"stuff\":\"test entry value number 1\"}"), + None, + &context1, + &vec![], + )) + .unwrap() + .address(); + thread::sleep(time::Duration::from_millis(500)); + + // Jill publishes another entry + context1 + .block_on(author_entry( + &test_entry_with_value("{\"stuff\":\"test entry value number 2\"}"), + None, + &context1, + &vec![], + )) + .unwrap() + .address(); + thread::sleep(time::Duration::from_millis(500)); + + // collect Jills local chain + let state = &context1.state().unwrap(); + let jill_headers: Vec = state + .agent() + .iter_chain() + .collect(); + let header = jill_headers.first().expect("Must be at least one header in chain"); + + // jack retrieves the top header addresss and reconstructs the Jills local chain by following the header back-links + let mut jack_headers: Vec = Vec::new(); + let mut next_header_addr = header.address(); + loop { + let mut entry: Option = None; + let mut tries = 0; + while entry.is_none() && tries < 10 { + tries = tries + 1; + { + entry = get_entry_from_dht(&context2, &next_header_addr).expect("Could not retrieve entry from DHT"); + } + println!("Try {}: {:?}", tries, entry); + if entry.is_none() { + thread::sleep(time::Duration::from_millis(1000)); + } + } + if let Some(Entry::ChainHeader(header)) = entry { + jack_headers.push(header.clone()); + if let Some(next_addr) = header.link() { + next_header_addr = next_addr + } else { + break // chain has been followed to the genesis entry + } + } else { + panic!(format!("Could not retrieve header at address: {}", next_header_addr)) + } + } + + assert_eq!( + jack_headers.len(), + 4, + ); + + assert_eq!( + jack_headers, + jill_headers, + ); + } +} \ No newline at end of file diff --git a/core_types/src/entry/mod.rs b/core_types/src/entry/mod.rs index c7c9811b04..e29da8bb1f 100644 --- a/core_types/src/entry/mod.rs +++ b/core_types/src/entry/mod.rs @@ -107,16 +107,21 @@ impl AddressableContent for Entry { fn address(&self) -> Address { match &self { Entry::AgentId(agent_id) => agent_id.address(), + Entry::ChainHeader(chain_header) => chain_header.address(), _ => Address::encode_from_str(&String::from(self.content()), Hash::SHA2256), } } fn content(&self) -> Content { - self.into() + match &self { + Entry::ChainHeader(chain_header) => chain_header.into(), + _ => self.into(), + } } fn try_from_content(content: &Content) -> JsonResult { Entry::try_from(content.to_owned()) + .or_else(|_| ChainHeader::try_from(content).map(|header| Entry::ChainHeader(header))) } } diff --git a/core_types/src/lib.rs b/core_types/src/lib.rs index 3842e05765..c97cbd6ed6 100644 --- a/core_types/src/lib.rs +++ b/core_types/src/lib.rs @@ -29,8 +29,8 @@ extern crate regex; #[macro_use] extern crate maplit; extern crate hcid; -extern crate wasmi; extern crate lib3h_protocol; +extern crate wasmi; pub mod chain_header; pub mod crud_status; pub mod eav; @@ -44,11 +44,11 @@ pub mod chain_migrate; pub mod dna; pub mod hdk_version; pub mod link; +pub mod network; pub mod signature; pub mod time; pub mod ugly; pub mod validation; -pub mod network; pub const GIT_HASH: &str = env!( "GIT_HASH", diff --git a/core_types/src/network/entry_aspect.rs b/core_types/src/network/entry_aspect.rs index efbbe73139..84275fd95a 100644 --- a/core_types/src/network/entry_aspect.rs +++ b/core_types/src/network/entry_aspect.rs @@ -1,5 +1,5 @@ -use chrono::{offset::FixedOffset, DateTime}; use crate::{chain_header::ChainHeader, entry::Entry, link::link_data::LinkData}; +use chrono::{offset::FixedOffset, DateTime}; use holochain_json_api::{error::JsonError, json::JsonString}; use holochain_persistence_api::cas::content::{Address, AddressableContent, Content}; use lib3h_protocol::data_types::EntryAspectData; @@ -163,8 +163,8 @@ impl fmt::Debug for EntryAspect { #[cfg(test)] pub mod tests { use super::*; - use chrono::{offset::FixedOffset, DateTime}; use crate::chain_header::test_chain_header; + use chrono::{offset::FixedOffset, DateTime}; #[test] fn can_convert_into_entry_aspect_data() { diff --git a/core_types/src/network/mod.rs b/core_types/src/network/mod.rs index 074287ab93..b77d7895e9 100644 --- a/core_types/src/network/mod.rs +++ b/core_types/src/network/mod.rs @@ -1,2 +1,2 @@ -pub mod query; pub mod entry_aspect; +pub mod query; diff --git a/core_types/src/network/query.rs b/core_types/src/network/query.rs index 64d0c9137b..bba1b4dc73 100644 --- a/core_types/src/network/query.rs +++ b/core_types/src/network/query.rs @@ -1,6 +1,4 @@ -use crate::{ - chain_header::ChainHeader, crud_status::CrudStatus, entry::EntryWithMetaAndHeader, -}; +use crate::{chain_header::ChainHeader, crud_status::CrudStatus, entry::EntryWithMetaAndHeader}; use holochain_json_api::{error::JsonError, json::JsonString}; use holochain_persistence_api::{cas::content::Address, eav::Value}; diff --git a/wasm_utils/src/api_serialization/get_links.rs b/wasm_utils/src/api_serialization/get_links.rs index 956caf4d57..0660e35c5f 100644 --- a/wasm_utils/src/api_serialization/get_links.rs +++ b/wasm_utils/src/api_serialization/get_links.rs @@ -38,7 +38,7 @@ impl Default for GetLinksOptions { } } -#[derive(Deserialize,Clone, Serialize, Debug, DefaultJson)] +#[derive(Deserialize, Clone, Serialize, Debug, DefaultJson)] pub struct LinksResult { pub address: Address, pub headers: Vec, @@ -46,7 +46,7 @@ pub struct LinksResult { pub status: CrudStatus, } -#[derive(Deserialize,Clone, Serialize, Debug, DefaultJson)] +#[derive(Deserialize, Clone, Serialize, Debug, DefaultJson)] pub struct GetLinksResult { links: Vec, } @@ -61,13 +61,11 @@ impl GetLinksResult { GetLinksResult { links } } - pub fn tags(&self) -> Vec - { + pub fn tags(&self) -> Vec { self.links.iter().map(|s| s.tag.clone()).collect() } - pub fn links(&self) -> Vec - { + pub fn links(&self) -> Vec { self.links.clone() }