Capability Tokens & P2P Functionality
### Zome Calls
- Zome calls have a unified security model
- Protects all zome calls independently of where they originated (externally or within the conductor)
- When the conductor receives a zome call request, it checks whether it complies with the security checks:
- If the call is signed by the agent with the public key for the cell, the call is accepted and executed
- If not, the conductor looks up the source chain for that cell for **capability tokens** for the requested function
- If some capability grant allows the request, it is executed
- Otherwise, the request is rejected
### Capability Tokens (I)
- Private entries that manage authorization to execute zome calls by sources external to the cell
- Two types of private entries:
- Capability Grants
- Stored by the grantor of the zome call request
- Allow an external source to execute a zome call request
- When they are deleted, that grant is no longer taken into account in the security checks
- This means that the cell is always in control of the grants that it makes to external sources
- Capability Claim
- Stored by the grantee of the capability
- They are not checked by the conductor to check whether an external zome call requests is valid
- Mostly a convenience entry type just to store information about the Capability Grant
- Both can be created as any other entry, at any point in time
### Capability Tokens (II)
#### Types of Capability Grant
- Unrestricted: disable security checks
- Any call to the function coming from any source will be executed
- This can be a huge security risk, so we generally don't grant unrestricted capability grants unless there is a really good reason
- Transferable: any source that knows a specific secret
- The zome call must carry a secret
- Only the calls that carry the secret specified in the capability token will be executed
- The secret can be stored in the source chain of the caller with a "Capability Claim" entry
- Assigned to specific agents
- The zome call must carry the agent public key of the caller and its signature and also a capability secret
- Only the calls that a) carry the secret specified and b) are made by one of the agents specified in the capability token are allowed to make the call
Capability Tokens (III)
Declaring the functions to grant capabilities for
use hdk::prelude::*;
#[hdk_extern]
fn zome_function_a(input: String) -> ExternResult<ActionHash> { // We want to grant
// a capability to
// execute this function
// Create, create links, get, query...
}
// Declares which functions we want to grant access to
fn functions_to_grant_capability_for() -> ExternResult<GrantedFunctions> { // Type required
// to specify
// the functions
let zome_name = zome_info()?.name; // Getting the zome name
let fn_name = FunctionName::from("zome_function_a"); // Just a wrapper around a "String"
// (String::from("zome_function_a"))
let mut functions: BTreeSet<(ZomeName, FunctionName)> = BTreeSet::new();
functions.insert((zome_name, fn_name)); // Grant access to the zome function
// "zome_function_a" of this zome
Ok(GrantedFunctions::Listed(functions)) // Can be "Listed" functions or "All" functions
}
Capability Tokens (IV)
Create unrestricted grant
use hdk::prelude::*;
#[hdk_extern]
fn grant_unrestricted_capability(_: ()) -> ExternResult<()> {
let functions = functions_to_grant_capability_for()?; // Declared in the previous slide
let access = CapAccess::Unrestricted; // Anyone can call this function,
// even unidentified sources
let capability_grant = CapGrantEntry { // Private entry built-in to Holochain and the HDK
functions,
access,
tag: String::from("unrestricted"), // Arbitrary humand readable tag, just for convenience
};
create_cap_grant(capability_grant)?; // Actually commit the capability
// From this point on, "zome_function_a"
// can be called from any source
Ok(())
}
Capability Tokens (V)
Create transferable grant
// Generate a random capability secret to give to a the granted agent
fn cap_secret() -> ExternResult<CapSecret> { // "CapSecret" is a wrapper around a byte array
let bytes = random_bytes(64)?; // "random_bytes" is a utility function from the HDK
let secret = CapSecret::try_from(bytes.into_vec())
.map_err(|_| wasm_error!(WasmErrorInner::Guest("Could not build secret".into())))?;
Ok(secret)
}
#[hdk_extern]
fn grant_transferable_capability(_: ()) -> ExternResult<CapSecret> {
let secret = cap_secret()?;
let access = CapAccess::Transferable {
secret, // Only requests that carry this secret will be able to execute "zome_function_a"
};
let capability_grant = CapGrantEntry {
functions: functions_to_grant_capability_for()?,
access,
tag: String::from("transferable capability"), // Convenience tag
};
create_cap_grant(capability_grant)?;
Ok(secret)
}
Capability Tokens (VI)
Create assigned grant
use hdk::prelude::*;
#[hdk_extern]
fn grant_assigned_capability(authorized_agent: AgentPubKey) -> ExternResult<()> {
/* To whom do we grant access to those functions? */
let mut assignees: BTreeSet<AgentPubKey> = BTreeSet::new(); // Type required by the API
// to specify the granted agents
assignees.insert(authorized_agent); // Assign capabilty to the given "authorized_agent"
let access = CapAccess::Assigned {
secret: cap_secret()?,
assignees // Only requests that **both** carry this secret and are signed by
// any of the assignees will be able to execute "zome_function_a"
};
let capability_grant = CapGrantEntry {
functions: functions_to_grant_capability_for()?,
access,
tag: String::from("assigned capability"), // Convenience tag
};
create_cap_grant(capability_grant)?;
Ok(())
}
Capability Tokens (VII)
Create capability claim
use hdk::prelude::*;
#[hdk_extern]
fn create_claim_for_zome_function_a(
grant_data: (AgentPubKey, CapSecret)
) -> ExternResult<()> { // The "CapSecret" would be provided by another process,
// maybe a bluetooth handshake
let cap_claim = CapClaimEntry { // Private entry built-in to Holochain and the HDK
grantor: grant_data.0, // Just to remember which agent to call
secret: grant_data.1, // Store the secret to be able to add it to the request
tag: String::from("claim for zome_function_a"), // Can be totally different from the
// tag in the capability grant
};
create_cap_claim(cap_claim)?; // Create the claim privately, nothing else happens
Ok(())
}
Capability Tokens (VIII)
Query the created capability claim
use hdk::prelude::*;
// Imagine we have already stored some CapClaims
// and we want to retrieve all stored CapClaims from a certain grantor
fn query_cap_claims_for(grantor: &AgentPubKey) -> ExternResult<Vec<CapClaim>> {
let claims_records = query(
ChainQueryFilter::new()
.entry_type(EntryType::CapClaim) // Only query Capability Claim related records
.include_entries(true), // Include the Capability Claim entries in the records
)?;
let claims_from_grantor: Vec<CapClaim> = claims_records
.into_iter()
.filter_map(|record| record.entry().as_option().cloned()) // Extract the entry
// from the record
.filter_map(|entry| match entry {
Entry::CapClaim(claim) => Some(claim.clone()), // Deserialize the entry to a "CapClaim"
_ => None,
})
.filter(|claim| claim.grantor.eq(&grantor)) // Only select the claims with
// the given grantor
.collect();
Ok(claims_from_grantor)
}
### 3 Sources of Zome Calls
- Conductor receives request on the application port
- Any process external to Holochain can bind to the application port of the conductor using a websocket connection
- Usually a web GUI
- Bridge call
- Any cell in a conductor can call another cell in the same conductor, by cell ID
- Remote call
- Any cell in a network can call another cell in the same network, by agent public key
Bridged call
#[hdk_extern]
pub fn store_file(file_bytes: SerializedBytes) -> ExternResult<EntryHash> {
let cell_role_name = String::from("file_storage_provider");
let zome_call_response = call(
CallTargetCell::OtherRole(cell_role_name), // "CallTargetCell" can be:
// - "OtherRole": must be one of the roles
// specified in the happ manifest
// - "OtherCell": call an arbitrary
// cell with its cell ID
// - "Local": calls the caller cell
ZomeName::from(String::from("file_storage")), // Name of t; he zome to call
FunctionName(String::from("store_file")), // Name of the zome function to call
None, // Capability secret, if necessary
file_bytes, // Input for the zome function
)?;
let agent_pubkey = agent_info()?.agent_latest_pubkey;
match zome_call_response {
ZomeCallResponse::Ok(result) => { // Of type ExternIO, wrapper around byte array
let entry_hash: EntryHash = result.decode() // ".decode()" deserializes the byte array,
// trying to interpret it as the requested typing
.map_err(|err| wasm_error!(String::from(err)))?;
Ok(entry_hash)
},
ZomeCallResponse::Unauthorized( // Callee deleted the capability grant before we called it
cell_id, zome_name, function_name, callee, agent_pubkey
) => {
Err(wasm_error!(WasmErrorInner::Guest("Agent revoked the capability".into())))
},
_ => {
Err(wasm_error!(WasmErrorInner::Guest(format!("There was an error by call: {:?}", zome_call_response))))
},
}
}
Remote call
use hdk::prelude::*;
// Defined in the "Capability Tokens (VIII) slide"
fn query_cap_claims_for(grantor: &AgentPubKey) -> ExternResult<Vec<CapClaim>> {}
// Call "zome_function_a" from the "callee"'s cell and return its result
#[hdk_extern]
fn call_zome_function_a(callee: AgentPubKey) -> ExternResult<ActionHash> {
let cap_claim_entry: CapClaimEntry = query_cap_claims_for(&callee)?
.first()
.cloned()
.ok_or(wasm_error!(WasmErrorInner::Guest(String::from("No CapClaims for this grantor"))))?;
let zome_call_response = call_remote(
callee, // The agent we are calling, must be a participant in this network
zome_name()?.name, // We are calling a function defined in this zome
FunctionName(String::from("zome_function_a")), // The name of the function we are calling
Some(cap_claim_entry.secret), // The "CapSecret" that we committed privately as the claim
String::from("example input") // Zome function input must derive "Serialize" and "Deserialize"
)?;
match zome_call_response { // "zome_call_response" is the same type than in the bridged call response
ZomeCallResponse::Ok(result) => {
let action_hash: ActionHash = result.decode().map_err(|err| wasm_error!(err))?;
Ok(action_hash)
},
ZomeCallResponse::NetworkError(err) => {
Err(wasm_error!(WasmErrorInner::Guest(format!("There was a network error: {:?}", err))))
},
_ => Err(wasm_error!(WasmErrorInner::Guest(
format!("Failed to handle remote call {:?}", zome_call_response)
))),
}
}
## call_info()
- HDK function
- Returns information about the source of the zome call that's being executed
use hdk::prelude::*;
#[hdk_extern]
fn request_retrieve_private_entries(_: ()) -> ExternResult<Record> { // This function could
// be called via
// call_remote
let info = call_info()?;
let caller: AgentPubKey = info.provenance;
match info.cap_grant {
CapGrant::ChainAuthor(my_pub_key) => {}, // The cell's agent called this function
CapGrant::RemoteAgent(zome_call_cap_grant) => {} // An external agent called this function
// via call or call_remote, and it was
// authorized because this
// "zome_call_cap_grant" exists in the
// source chain for this cell
}
// ... execute the rest of the code
}
### Signals
- Don't wait for a response: fire and forget
- Not reliable: signals can be lost without our code knowing
- 2 types:
- Remote Signals
- Concurrent call to multiple agents
- Very similar to "call_remote"
- Useful for notifications
- UI Signals
- Send message from the conductor to all processes that are listening
Remote Signals
use hdk::prelude::*;
#[hdk_extern]
fn notify(agents_to_notify: Vec<AgentPubKey>) -> ExternResult<()> {
let input_string = String::from("input parameter");
remote_signal(input_string, agents_to_notify)?; // Doesn't wait for the agents
// to receive the signal
Ok(())
}
#[hdk_extern]
fn recv_remote_signal(signal: String) -> ExternResult<()> { // Callback that holochain
// will execute in the
// callee's cell with
// the received signal
// Handle the signal in the appropriate way
}
UI Signals
use hdk::prelude::*;
#[hdk_extern]
fn notify_ui(_: ()) -> ExternResult<()> {
let input_string = String::from("input parameter");
emit_signal(input_string)?; // No other parameter other than the input because
// all the processes connected to the conductor
// will be notified
Ok(())
}