Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/guide/app-struct…
Browse files Browse the repository at this point in the history
…ure-dnas
  • Loading branch information
pdaoust committed Feb 3, 2025
2 parents 4557f70 + 3d0f54e commit 6d20e39
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 54 deletions.
6 changes: 6 additions & 0 deletions code_test_4/web-happ.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest_version: '1'
name: blup
ui:
bundled: ./path/to/my/ui.zip
happ_manifest:
bundled: ./path/to/my/happ-bundle.happ
83 changes: 33 additions & 50 deletions src/pages/build/callbacks-and-lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ All of the callbacks must follow the [pattern for public functions](/build/zomes

## Integrity zomes

Your [integrity zome](/build/zomes/#integrity) may define three callbacks, `validate`, `entry_defs`, and `genesis_self_check`. All of these functions **cannot have side effects**; any attempt to write data will fail. They also cannot access data that changes over time or across agents, such as the current cell's [agent ID](/build/identifiers/#agent) or a collection of [links](/build/links-paths-and-anchors/) in the [DHT](/concepts/4_dht).

Your [integrity zome](/build/zomes/#integrity) may define two callbacks, `validate` and `genesis_self_check`. These functions **cannot have side effects**; any attempt to write data will fail. They also cannot access data that changes over time or across agents, such as the current cell's [agent ID](/build/identifiers/#agent) or a collection of [links](/build/links-paths-and-anchors/) in the [DHT](/concepts/4_dht).

### Define a `validate` callback

In order to validate your data you'll need to define a `validate` callback. It must take a single argument of type [`Op`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/op/enum.Op.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`.
In order to validate DHT data, you'll need to define a `validate` callback. It must take a single argument of type [`Op`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/op/enum.Op.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`.

The `validate` callback is called at two times:

Expand All @@ -33,26 +32,14 @@ pub fn validate(_: Op) -> ExternResult<ValidateCallbackResult> {
}
```

And here's an example of one that rejects everything. You'll note that the outer result is `Ok`; you should generally reserve `Err` for unexpected failures such as inability to deserialize data. However, Holochain will treat both `Ok(Invalid)` and `Err` as invalid operations that should be rejected.

```rust
use hdi::prelude::*;

#[hdk_extern]
pub fn validate(_: Op) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Invalid("I reject everything".into()))
}
```

### Define an `entry_defs` callback

You don't need to write this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section under Entries](/build/entries/#define-an-entry-type) to find out how.

### Define a `genesis_self_check` callback

Holochain assumes that every agent is able to self-validate all the data they create before storing it in their [source chain](/concepts/3_source_chain/) and publishing it to the [DHT](/concepts/4_dht/). But at **genesis** time, when their cell has just been instantiated but they haven't connected to other peers, they may not be able to fully validate their [**genesis records**](/concepts/3_source_chain/#source-chain-your-own-data-store) if their validity depends on shared data. So Holochain skips full self-validation for these records, only validating the basic structure of their [actions](/build/working-with-data/#entries-actions-and-records-primary-data).
If your network needs to control who can and can't join it, you can write your DNA to require a [**membrane proof**](/resources/glossary/#membrane-proof), a small chunk of bytes that the app receives from the user at installation time and stores on the source chain. You can then write validation rules for this record just like any other record.

This creates a risk to the new agent; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/resources/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example.
There's one challenge with this: validation requires access to the network (a membrane proof may reference DHT data that contains a list of public keys authorized to grant access to newcomers, for instance), but the membrane proof is written at [**genesis**](/resources/glossary/#genesis-records) time when the cell doesn't have network access yet. So Holochain can't do the usual self-validation before publishing it.

This means someone could accidentally type or paste a malformed membrane proof and be rejected from the network. To guard against this, you can define a `genesis_self_check` function that runs at genesis time and checks the content of the membrane proof before it's written.

`genesis_self_check` must take a single argument of type [`GenesisSelfCheckData`](https://docs.rs/hdi/latest/hdi/prelude/type.GenesisSelfCheckData.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`.

Expand All @@ -75,7 +62,7 @@ pub fn genesis_self_check(data: GenesisSelfCheckData) -> ExternResult<ValidateCa

## Coordinator zomes

A [coordinator zome](/build/zomes/#coordinator) may define some lifecycle hooks: `init`, `post_commit`, and `recv_remote_signal`.
A [coordinator zome](/build/zomes/#coordinator) may define some callbacks: `init`, `post_commit`, and `recv_remote_signal`.

### Define an `init` callback

Expand All @@ -93,16 +80,16 @@ You can force `init` to run eagerly by calling it as if it were a normal zome fu

Once `init` runs successfully for all coordinator zomes in a DNA, Holochain writes an [`InitZomesComplete` action](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.InitZomesComplete.html). That ensures that this callback isn't called again.

`init` must take an empty `()` input argument and return an [`InitCallbackResult`](https://docs.rs/hdk/latest/hdk/prelude/enum.InitCallbackResult.html) wrapped in an `ExternResult`. All zomes' `init` callbacks in a DNA must return a success result in order for cell initialization to succeed; otherwise any data written in these callbacks, along with the `InitZomesComplete` action, will be rolled back. _If any zome's init callback returns an `InitCallbackResult::Fail`, initialization will fail._ Otherwise, if any init callback returns an `InitCallbackResult::UnresolvedDependencies`, initialization will be retried at the next zome call attempt.
`init` takes no arguments and must return an [`InitCallbackResult`](https://docs.rs/hdk/latest/hdk/prelude/enum.InitCallbackResult.html) wrapped in an `ExternResult`. All zomes' `init` callbacks in a DNA must return a success result in order for cell initialization to succeed; otherwise any data written in these callbacks, along with the `InitZomesComplete` action, will be rolled back. _If any zome's init callback returns an `InitCallbackResult::Fail`, initialization will fail._ Otherwise, if any init callback returns an `InitCallbackResult::UnresolvedDependencies`, initialization will be retried at the next zome call attempt.

Here's an `init` callback that [links](/build/links-paths-and-anchors/) the [agent's ID](/build/identifiers/#agent) to the [DNA hash](/build/identifiers/#dna) as a sort of "I'm here" note. (It depends on a couple things being defined in your integrity zome; we'll show the integrity zome after this sample for completeness.)

```rust
use foo_integrity::{get_participant_registration_anchor, LinkTypes};
use foo_integrity::{get_participant_registration_anchor_hash, LinkTypes};
use hdk::prelude::*;

#[hdk_extern]
pub fn init(_: ()) -> ExternResult<InitCallbackResult> {
pub fn init() -> ExternResult<InitCallbackResult> {
let participant_registration_anchor_hash = get_participant_registration_anchor_hash()?;
let AgentInfo { agent_latest_pubkey: my_pubkey, ..} = agent_info()?;
create_link(
Expand All @@ -116,41 +103,30 @@ pub fn init(_: ()) -> ExternResult<InitCallbackResult> {
}
```

Here's the `foo_integrity` zome code needed to make this work:
Here's the `foo_integrity` zome code needed to make this work. It uses something called 'paths', which we [talk about elsewhere](/build/links-paths-and-anchors/#anchors-and-paths).

```rust
use hdi::prelude::*
use hdi::prelude::*;

#[hdk_link_types]
pub enum LinkTypes {
ParticipantRegistration,
}

// This is a very simple implementation of the Anchor pattern, which you can
// read about in https://developer.holochain.org/build/links-paths-and-anchors/
// You don't need to tell Holochain about it with the `hdk_entry_types` macro,
// because it never gets stored -- we only use it to calculate a hash.
#[hdk_entry_helper]
pub struct Anchor(pub Vec<u8>);

pub fn get_participant_registration_anchor_hash() -> ExternResult<EntryHash> {
hash_entry(Anchor(
"_participants_"
.as_bytes()
.to_owned()
))
Path(vec!["_participants_".into()]).path_entry_hash()
}
```

!!! info Why link the agent key to a well-known hash?

Because there's no single source of truth in a Holochain network, it's impossible to get the full list of agents who have joined it. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them.
There's no such thing as a 'users table' in a Holochain DHT. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them.

But if there are a lot of agents in the network, this can create "hot spots" where one set of agents --- the ones responsible for storing the base address for all those links --- carry an outsized burden compared to other agents. Read the [anchors and paths section under Links, Paths, and Anchors](/build/links-paths-and-anchors/#anchors-and-paths) for more info.
Note that this can create "hot spots" where some agents have a heavier data storage and network traffic burden than others. Read the [anchors and paths section under Links, Paths, and Anchors](/build/links-paths-and-anchors/#anchors-and-paths) for more info.

!!!

This `init` callback also does something useful: it grants all peers in the network permission to send messages to an agent's [remote signal receiver callback](#define-a-recv-remote-signal-callback).
This `init` callback also does something useful: it grants all peers in the network permission to send messages to an agent's [remote signal receiver callback](#define-a-recv-remote-signal-callback). (Note that this can create a risk of spamming.) {#init-grant-unrestricted-access-to-recv-remote-signal}

```rust
use hdk::prelude::*;
Expand All @@ -173,7 +149,7 @@ pub fn init(_: ()) -> ExternResult<InitCallbackResult> {

<!-- TODO: move this to the signals page after it's written -->

Agents in a network can send messages to each other via [remote signals](/concepts/9_signals/#remote-signals). In order to handle these signals, your coordinator zome needs to define a `recv_remote_signal` callback. Remote signals get routed from the emitting coordinator zome on the sender's machine to the same one on the receiver's machine, so there's no need for a coordinator to handle message types it doesn't know about.
Agents in a network can send messages to each other via [remote signals](/concepts/9_signals/#remote-signals). In order to handle these signals, your coordinator zome needs to define a `recv_remote_signal` callback. Remote signals get routed from the emitting coordinator zome on the sender's machine to a coordinator with the same name on the receiver's machine.

`recv_remote_signal` takes a single argument of any type you like --- if your coordinator zome deals with multiple message types, consider creating an enum for all of them. It must return an empty `ExternResult<()>`, as this callback is not called as a result of direct interaction from the local agent and has nowhere to pass a return value.

Expand All @@ -186,6 +162,7 @@ use hdk::prelude::*;
// We're creating this type for both remote signals to other peers and local
// signals to the UI.
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Signal {
Heartbeat(AgentPubKey),
}
Expand All @@ -195,7 +172,7 @@ pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> {
if let Signal::Heartbeat(agent_id) = payload {
// Pass the heartbeat along to my UI so it can update the other
// peer's online status.
return emit_signal(Signal::Heartbeat(agent_id));
emit_signal(Signal::Heartbeat(agent_id))?;
}
Ok(())
}
Expand Down Expand Up @@ -229,6 +206,10 @@ pub fn heartbeat(_: ()) -> ExternResult<()> {
}
```

!!! info Remote signals and privileges
If you grant unrestricted access to your remote signal callback like in the [previous example](#init-grant-unrestricted-access-to-recv-remote-signal), take care that it does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon.<!-- TODO: link when the capabilities page is written -->
!!!

### Define a `post_commit` callback

After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored yet, even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok` with the [hash of the newly written action](/build/identifiers/#the-unpredictability-of-action-hashes). (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) That means that any follow-up you do within the same function, like pinging other peers, might point to data that doesn't exist if the function fails at a later step.
Expand All @@ -237,6 +218,8 @@ If you need to do any follow-up, it's safer to do this in a lifecycle hook calle

`post_commit` must take a single argument of type <code>Vec&lt;<a href="https://docs.rs/hdk/latest/hdk/prelude/type.SignedActionHashed.html">SignedActionHashed</a>&gt;</code>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback **must not write any data**, but it may call other zome functions in the same cell or any other local or remote cell, and it may send local or remote signals.

`post_commit` also can't return an error. There should be no return type, and it should handle all errors it receives from other functions. It also must be tagged with `#[hdk_extern(infallible)]`.

Here's an example that uses `post_commit` to tell someone a movie loan has been created for them. It uses the integrity zome examples from [Identifiers](/build/identifiers/#in-dht-data).

```rust
Expand All @@ -248,19 +231,19 @@ pub enum RemoteSignal {
MovieLoanHasBeenCreatedForYou(ActionHash),
}

#[hdk_extern]
pub fn post_commit(actions: Vec<SignedActionHashed>) -> ExternResult<()> {
#[hdk_extern(infallible)]
pub fn post_commit(actions: Vec<SignedActionHashed>) {
for action in actions.iter() {
// Only handle cases where an entry is being created.
if let Action::Create(_) = action.action() {
let movie_loan = get_movie_loan(action.action_address().clone())?;
return send_remote_signal(
RemoteSignal::MovieLoanHasBeenCreatedForYou(action.action_address().clone()),
vec![movie_loan.lent_to]
);
if let Ok(movie_loan) = get_movie_loan(action.action_address().clone()) {
send_remote_signal(
RemoteSignal::MovieLoanHasBeenCreatedForYou(action.action_address().clone()),
vec![movie_loan.lent_to]
).ok(); // suppress warning about unhandled `Result`
}
}
}
Ok(())
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down
2 changes: 1 addition & 1 deletion src/pages/build/entries.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ let create_action_hash = create_entry(

If your entry doesn't have any dependencies on other data, you can use [relaxed chain top ordering](/build/zome-functions/#relaxed-chain-top-ordering) to prevent possible transaction rollbacks (we'll let that page explain when this could happen and how to design around it).

To use this feature, you'll need to use the more low-level [`create`](https://docs.rs/hdk/latest/hdk/entry/fn.create.html) host function, which requires you to build a more complex input. This example batches updates to director entries, which don't have reference other data including each other, so they're a good candidate for relaxed ordering.
To use this feature, you'll need to use the more low-level [`create`](https://docs.rs/hdk/latest/hdk/entry/fn.create.html) host function, which requires you to build a more complex input. This example batches updates to director entries, which don't have to reference other data including each other, so they're a good candidate for relaxed ordering.

```rust
use movie_integrity::{Director, EntryTypes};
Expand Down
4 changes: 2 additions & 2 deletions src/pages/concepts/3_source_chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The user's actions are stored in the source chain as **records**, which consist

It's most helpful to think of an action as just that --- the _act of changing application state_. So instead of thinking of a source chain record as "a chat message", for instance, you could think of it as recording "the act of adding a chat message to your feed". So while it's useful for noun-like things like messages and images, it is also well-suited to things like real-time document edits, game moves, and transactions. You'll learn about all the different action types in the section on [CRUD](../6_crud_actions/) --- actions like 'create link' and 'delete entry'.

This journal starts with three special system records, followed optionally by some app data, and one final system record:
This journal starts with three special system records called **genesis records**, followed optionally by some app data, and one final system record:

![](/assets/img/concepts/3.3-genesis-records-1-and-2.png){.sz60p} {.center}

Expand All @@ -64,7 +64,7 @@ This journal starts with three special system records, followed optionally by so
2. The agent's **membrane proof**. When a cell tries to join a network, it shares this entry with the existing peers, who check it and determine whether the cell should be allowed to join them. Examples: an invite code, an employee ID signed by the HR department, or a proof of paid subscription fees.
3. The **agent ID**. This contains your public key as your digital identity. The signatures on all subsequent records must match this public key in order to be valid.
4. Zero or more records containing application data that was written during the init process --- that is, by the `init` [lifecycle hook](../11_lifecycle_events) of any coordinator zomes that define one.
4. The **init complete** action. This is a record meant for internal use, simply helping the conductor remind itself that it's completely activated the cell by running all the coordinator zomes' `init` callbacks.
5. The **init complete** action. This is a record meant for internal use, simply helping the conductor remind itself that it's completely activated the cell by running all the coordinator zomes' `init` callbacks.

After this come the records that record the user's actions. These include:

Expand Down
1 change: 0 additions & 1 deletion src/pages/resources/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,6 @@ The four records at the beginning of an [agent's](#agent) [source chain](#source
1. The [DNA hash](#dna-hash), which shows that the agent has seen the network's rules and agrees to abide by them,
2. The [membrane proof](#membrane-proof), which the agent presents as a claim that they should be allowed to join the [DHT](#distributed-hash-table-dht),
3. The [agent ID](#agent-id), which advertises the agent's [public key](#public-key-cryptography),
4. The [init complete action](#init-complete-action), which tells the conductor that all the DNA's [init callbacks](#init-callback) have completed successfully and the source chain is ready to have [app entries](#app-entry) written to it.

#### Global consensus

Expand Down

0 comments on commit 6d20e39

Please sign in to comment.