Skip to content

Commit

Permalink
Transitive linking validation
Browse files Browse the repository at this point in the history
  • Loading branch information
guillemcordoba committed Oct 29, 2024
1 parent e665129 commit cd0d820
Show file tree
Hide file tree
Showing 10 changed files with 821 additions and 48 deletions.
268 changes: 267 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion crates/linked_devices_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ version = "0.1.0"
edition = "2021"

[dependencies]
hdi = { workspace = true }
hdi = { workspace = true, features = [ "test_utils"] }
serde = "1"

[dev-dependencies]
fixt = "0.3"
# holo_hash= { version = "0.3", features = [ "test_utils"] }
192 changes: 192 additions & 0 deletions crates/linked_devices_types/src/are_agents_linked.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::collections::BTreeMap;

use hdi::prelude::*;

use crate::{LinkedDevices, LinkedDevicesProof};

pub fn are_agents_linked(
agent_1: &AgentPubKey,
agent_2: &AgentPubKey,
proofs: &Vec<LinkedDevicesProof>,
) -> bool {
for proof in proofs {
if proof.linked_devices.agents.contains(agent_1)
&& proof.linked_devices.agents.contains(agent_2)
{
return true;
}
}

let all_linked_devices: Vec<LinkedDevices> =
proofs.iter().map(|p| p.linked_devices.clone()).collect();

let mut all_links: BTreeMap<AgentPubKey, HashSet<AgentPubKey>> = BTreeMap::new();

for linked_devices in all_linked_devices {
for agent_a in &linked_devices.agents {
for agent_b in &linked_devices.agents {
if agent_a.ne(&agent_b) {
all_links
.entry(agent_a.clone())
.or_insert(HashSet::new())
.insert(agent_b.clone());
all_links
.entry(agent_b.clone())
.or_insert(HashSet::new())
.insert(agent_a.clone());
}
}
}
}

if !all_links.contains_key(&agent_1) || !all_links.contains_key(&agent_2) {
return false;
}

let mut current_node = agent_1.clone();
let mut current_path: Vec<AgentPubKey> = vec![agent_1.clone()];
let mut visited: HashSet<AgentPubKey> = HashSet::new();
visited.insert(current_node.clone());
let mut all_paths_explored = false;

while !all_paths_explored {
if current_node.eq(agent_2) {
return true;
}

let neighbors = all_links
.get(&current_node)
.expect("Unreachable: we just added all neighbors the map");

let next_node = neighbors
.iter()
.find(|neighbor| !visited.contains(&neighbor));

match next_node {
Some(next_node) => {
current_node = next_node.clone();
visited.insert(next_node.clone());
current_path.push(next_node.clone());
}
None => {
current_path.pop();
match current_path.last() {
Some(node) => {
current_node = node.clone();
}
None => {
all_paths_explored = true;
}
}
}
}
}

false
}

#[cfg(test)]
mod tests {
use fixt::fixt;
use hdi::prelude::fixt::AgentPubKeyFixturator;
use hdi::prelude::Timestamp;

use super::are_agents_linked;
use crate::{LinkedDevices, LinkedDevicesProof};

#[test]
fn basic_linked_test() {
let alice = fixt!(AgentPubKey);
let bob = fixt!(AgentPubKey);

let linked_devices = LinkedDevices {
agents: vec![alice.clone(), bob.clone()],
timestamp: Timestamp::max(),
};
let proof = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

assert!(are_agents_linked(&alice, &bob, &vec![proof]));
}

#[test]
fn unlinked_test() {
let alice = fixt!(AgentPubKey);
let bob = fixt!(AgentPubKey);
let carol = fixt!(AgentPubKey);
let dave = fixt!(AgentPubKey);

let linked_devices = LinkedDevices {
agents: vec![alice.clone(), bob.clone()],
timestamp: Timestamp::max(),
};
let proof_1 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

let linked_devices = LinkedDevices {
agents: vec![carol.clone(), dave.clone()],
timestamp: Timestamp::max(),
};
let proof_2 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

assert!(!are_agents_linked(&alice, &dave, &vec![proof_1, proof_2]));
}

#[test]
fn transitive_linked_test() {
let alice = fixt!(AgentPubKey);
let bob = fixt!(AgentPubKey);
let carol = fixt!(AgentPubKey);
let dave = fixt!(AgentPubKey);
let edward = fixt!(AgentPubKey);

let linked_devices = LinkedDevices {
agents: vec![alice.clone(), bob.clone()],
timestamp: Timestamp::max(),
};
let proof_1 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

let linked_devices = LinkedDevices {
agents: vec![bob.clone(), carol.clone()],
timestamp: Timestamp::max(),
};
let proof_2 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

let linked_devices = LinkedDevices {
agents: vec![carol.clone(), dave.clone()],
timestamp: Timestamp::max(),
};
let proof_3 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

let linked_devices = LinkedDevices {
agents: vec![dave.clone(), edward.clone()],
timestamp: Timestamp::max(),
};
let proof_4 = LinkedDevicesProof {
linked_devices,
signatures: vec![],
};

assert!(are_agents_linked(
&alice,
&edward,
&vec![proof_1, proof_2, proof_3, proof_4]
));
}
}
20 changes: 4 additions & 16 deletions crates/linked_devices_types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use hdi::prelude::*;

mod are_agents_linked;
pub use are_agents_linked::are_agents_linked;

#[derive(Serialize, Deserialize, Debug, SerializedBytes)]
pub struct AgentToLinkedDevicesLinkTag(pub Vec<LinkedDevicesProof>);

#[derive(Serialize, Deserialize, Debug, SerializedBytes)]
#[derive(Serialize, Deserialize, Debug, SerializedBytes, Clone)]
pub struct LinkedDevicesProof {
pub linked_devices: LinkedDevices,
pub signatures: Vec<Signature>,
Expand Down Expand Up @@ -109,18 +112,3 @@ fn has_linked_device(

Ok(agent_to_linked_device.is_some())
}

pub fn are_agents_linked(
agent_1: &AgentPubKey,
agent_2: &AgentPubKey,
proofs: &Vec<LinkedDevicesProof>,
) -> bool {
for proof in proofs {
if proof.linked_devices.agents.contains(agent_1)
&& proof.linked_devices.agents.contains(agent_2)
{
return true;
}
}
false
}
20 changes: 19 additions & 1 deletion tests/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export async function setup(scenario: Scenario) {

// Add 2 players with the test hApp to the Scenario. The returned players
// can be destructured.
const [alice, bob] = await scenario.addPlayersWithApps([
const [alice, bob, carol] = await scenario.addPlayersWithApps([
{ appBundleSource: { path: testHappUrl } },
{ appBundleSource: { path: testHappUrl } },
{ appBundleSource: { path: testHappUrl } },
]);
Expand All @@ -42,6 +43,10 @@ export async function setup(scenario: Scenario) {
.adminWs()
.authorizeSigningCredentials(bob.cells[0].cell_id);

await carol.conductor
.adminWs()
.authorizeSigningCredentials(carol.cells[0].cell_id);

// Shortcut peer discovery through gossip and register all agents in every
// conductor of the scenario.
await scenario.shareAllAgents();
Expand All @@ -62,13 +67,22 @@ export async function setup(scenario: Scenario) {
),
);

const carolStore = new LinkedDevicesStore(
new LinkedDevicesClient(
carol.appWs as any,
'linked_devices_test',
'linked_devices',
),
);

// Shortcut peer discovery through gossip and register all agents in every
// conductor of the scenario.
await scenario.shareAllAgents();

// Prevent race condition when two zome calls are made instantly at the beginning of the lifecycle that cause a ChainHeadMoved error because they trigger 2 parallel init workflows
await aliceStore.client.getLinkingAgents();
await bobStore.client.getLinkingAgents();
await carolStore.client.getLinkingAgents();

return {
alice: {
Expand All @@ -79,5 +93,9 @@ export async function setup(scenario: Scenario) {
player: bob,
store: bobStore,
},
carol: {
player: carol,
store: carolStore,
},
};
}
Loading

0 comments on commit cd0d820

Please sign in to comment.