From e6651293a8bb61e1b9a595dad69009037ffd1544 Mon Sep 17 00:00:00 2001 From: "guillem.cordoba" Date: Mon, 28 Oct 2024 16:58:04 +0100 Subject: [PATCH] Tests passing --- docs/{link-device-process.md => design.md} | 2 +- tests/src/agent-to-linked-devices.test.ts | 39 ----- tests/src/link-devices.test.ts | 58 ++++++++ tests/src/setup.ts | 135 ++++++++++-------- ui/src/linked-devices-client.ts | 25 +++- ui/src/linked-devices-store.ts | 5 +- .../linked_devices/src/link_devices.rs | 50 +++++-- .../src/agent_to_linked_devices.rs | 7 +- zomes/integrity/linked_devices/src/lib.rs | 45 ++++-- 9 files changed, 237 insertions(+), 129 deletions(-) rename docs/{link-device-process.md => design.md} (98%) delete mode 100644 tests/src/agent-to-linked-devices.test.ts create mode 100644 tests/src/link-devices.test.ts diff --git a/docs/link-device-process.md b/docs/design.md similarity index 98% rename from docs/link-device-process.md rename to docs/design.md index 999f339..5fa2092 100644 --- a/docs/link-device-process.md +++ b/docs/design.md @@ -1,4 +1,4 @@ - +# Link Devices process ```mermaid sequenceDiagram diff --git a/tests/src/agent-to-linked-devices.test.ts b/tests/src/agent-to-linked-devices.test.ts deleted file mode 100644 index 47e92c0..0000000 --- a/tests/src/agent-to-linked-devices.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { toPromise } from '@holochain-open-dev/signals'; -import { EntryRecord } from '@holochain-open-dev/utils'; -import { ActionHash, Record } from '@holochain/client'; -import { dhtSync, runScenario } from '@holochain/tryorama'; -import { decode } from '@msgpack/msgpack'; -import { assert, test } from 'vitest'; - -import { setup } from './setup.js'; - -test('link devices', async () => { - await runScenario(async scenario => { - const { alice, bob } = await setup(scenario); - - const baseAddress = alice.player.agentPubKey; - const targetAddress = alice.player.agentPubKey; - - // Bob gets the links, should be empty - let linksOutput = await toPromise( - bob.store.linkedDevicesForAgent.get(baseAddress), - ); - assert.equal(linksOutput.length, 0); - - // Alice creates a link from Agent to LinkedDevice - await alice.store.client.addLinkedDeviceForAgent( - baseAddress, - targetAddress, - ); - - // Wait for the created entry to be propagated to the other node. - await dhtSync([alice.player, bob.player], alice.player.cells[0].cell_id[0]); - - // Bob gets the links again - linksOutput = await toPromise( - bob.store.linkedDevicesForAgent.get(baseAddress), - ); - assert.equal(linksOutput.length, 1); - assert.deepEqual(targetAddress, linksOutput[0]); - }); -}); diff --git a/tests/src/link-devices.test.ts b/tests/src/link-devices.test.ts new file mode 100644 index 0000000..9f5d1b1 --- /dev/null +++ b/tests/src/link-devices.test.ts @@ -0,0 +1,58 @@ +import { toPromise } from '@holochain-open-dev/signals'; +import { EntryRecord } from '@holochain-open-dev/utils'; +import { ActionHash, Record, encodeHashToBase64 } from '@holochain/client'; +import { dhtSync, runScenario } from '@holochain/tryorama'; +import { decode } from '@msgpack/msgpack'; +import { assert, test } from 'vitest'; + +import { setup } from './setup.js'; + +test('link devices', async () => { + await runScenario(async scenario => { + const { alice, bob } = await setup(scenario); + + // Bob gets the links, should be empty + let linksOutput = await toPromise( + bob.store.linkedDevicesForAgent.get(bob.player.agentPubKey), + ); + assert.equal(linksOutput.length, 0); + + const alicePasscode = [1, 3, 7, 2]; + const bobPasscode = [9, 3, 8, 4]; + + await alice.store.client.prepareLinkDevices(alicePasscode); + await bob.store.client.prepareLinkDevices(bobPasscode); + + await alice.store.client.initLinkDevices( + bob.player.agentPubKey, + bobPasscode, + ); + await bob.store.client.requestLinkDevices( + alice.player.agentPubKey, + alicePasscode, + ); + + // Wait for the created entry to be propagated to the other node. + await dhtSync([alice.player, bob.player], alice.player.cells[0].cell_id[0]); + + // Bob gets the links again + linksOutput = await toPromise( + bob.store.linkedDevicesForAgent.get(bob.player.agentPubKey), + ); + assert.equal(linksOutput.length, 1); + assert.deepEqual( + encodeHashToBase64(alice.player.agentPubKey), + encodeHashToBase64(linksOutput[0]), + ); + + // Alice gets the links again + linksOutput = await toPromise( + alice.store.linkedDevicesForAgent.get(alice.player.agentPubKey), + ); + assert.equal(linksOutput.length, 1); + assert.deepEqual( + encodeHashToBase64(bob.player.agentPubKey), + encodeHashToBase64(linksOutput[0]), + ); + }); +}); diff --git a/tests/src/setup.ts b/tests/src/setup.ts index 50c609a..bfb3c93 100644 --- a/tests/src/setup.ts +++ b/tests/src/setup.ts @@ -1,60 +1,83 @@ -import { EntryRecord } from "@holochain-open-dev/utils"; +import { EntryRecord } from '@holochain-open-dev/utils'; import { - ActionHash, - AgentPubKey, - AppBundleSource, - AppCallZomeRequest, - AppWebsocket, - encodeHashToBase64, - EntryHash, - fakeActionHash, - fakeAgentPubKey, - fakeDnaHash, - fakeEntryHash, - NewEntryAction, - Record, -} from "@holochain/client"; -import { Scenario } from "@holochain/tryorama"; -import { encode } from "@msgpack/msgpack"; -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { LinkedDevicesClient } from "../../ui/src/linked-devices-client.js"; -import { LinkedDevicesStore } from "../../ui/src/linked-devices-store.js"; + ActionHash, + AgentPubKey, + AppBundleSource, + AppCallZomeRequest, + AppWebsocket, + EntryHash, + NewEntryAction, + Record, + encodeHashToBase64, + fakeActionHash, + fakeAgentPubKey, + fakeDnaHash, + fakeEntryHash, +} from '@holochain/client'; +import { Scenario } from '@holochain/tryorama'; +import { encode } from '@msgpack/msgpack'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +import { LinkedDevicesClient } from '../../ui/src/linked-devices-client.js'; +import { LinkedDevicesStore } from '../../ui/src/linked-devices-store.js'; export async function setup(scenario: Scenario) { - const testHappUrl = dirname(fileURLToPath(import.meta.url)) + "/../../workdir/linked-devices_test.happ"; - - // Add 2 players with the test hApp to the Scenario. The returned players - // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([ - { appBundleSource: { path: testHappUrl } }, - { appBundleSource: { path: testHappUrl } }, - ]); - - // Shortcut peer discovery through gossip and register all agents in every - // conductor of the scenario. - await scenario.shareAllAgents(); - - const aliceStore = new LinkedDevicesStore( - new LinkedDevicesClient(alice.appWs as any, "linked_devices_test", "linked_devices"), - ); - - const bobStore = new LinkedDevicesStore( - new LinkedDevicesClient(bob.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(); - - return { - alice: { - player: alice, - store: aliceStore, - }, - bob: { - player: bob, - store: bobStore, - }, - }; + const testHappUrl = + dirname(fileURLToPath(import.meta.url)) + + '/../../workdir/linked-devices_test.happ'; + + // Add 2 players with the test hApp to the Scenario. The returned players + // can be destructured. + const [alice, bob] = await scenario.addPlayersWithApps([ + { appBundleSource: { path: testHappUrl } }, + { appBundleSource: { path: testHappUrl } }, + ]); + + await alice.conductor + .adminWs() + .authorizeSigningCredentials(alice.cells[0].cell_id); + + await bob.conductor + .adminWs() + .authorizeSigningCredentials(bob.cells[0].cell_id); + + // Shortcut peer discovery through gossip and register all agents in every + // conductor of the scenario. + await scenario.shareAllAgents(); + + const aliceStore = new LinkedDevicesStore( + new LinkedDevicesClient( + alice.appWs as any, + 'linked_devices_test', + 'linked_devices', + ), + ); + + const bobStore = new LinkedDevicesStore( + new LinkedDevicesClient( + bob.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(); + + return { + alice: { + player: alice, + store: aliceStore, + }, + bob: { + player: bob, + store: bobStore, + }, + }; } diff --git a/ui/src/linked-devices-client.ts b/ui/src/linked-devices-client.ts index a1f9ae4..8584a88 100644 --- a/ui/src/linked-devices-client.ts +++ b/ui/src/linked-devices-client.ts @@ -1,5 +1,10 @@ import { ZomeClient } from '@holochain-open-dev/utils'; -import { AgentPubKey, AppClient, Link } from '@holochain/client'; +import { + AgentPubKey, + AppCallZomeRequest, + AppClient, + Link, +} from '@holochain/client'; import { LinkedDevicesProof, LinkedDevicesSignal } from './types.js'; @@ -28,8 +33,8 @@ export class LinkedDevicesClient extends ZomeClient { }); } - async prepareLinkDevices(passcode: number[]) { - await this.callZome('prepare_link_devices', passcode); + async prepareLinkDevices(myPasscode: number[]) { + await this.callZome('prepare_link_devices', myPasscode); } async getLinkingAgents(): Promise> { return this.callZome('get_linking_agents', null); @@ -39,10 +44,16 @@ export class LinkedDevicesClient extends ZomeClient { } async initLinkDevices(recipient: AgentPubKey, recipient_passcode: number[]) { - await this.callZome('init_link_devices', { - recipient, - recipient_passcode, - }); + const req: AppCallZomeRequest = { + role_name: this.roleName, + zome_name: this.zomeName, + fn_name: 'init_link_devices', + payload: { + recipient, + recipient_passcode, + }, + }; + await this.client.callZome(req, 2_000); } async requestLinkDevices( diff --git a/ui/src/linked-devices-store.ts b/ui/src/linked-devices-store.ts index 9f65cec..1d0c7eb 100644 --- a/ui/src/linked-devices-store.ts +++ b/ui/src/linked-devices-store.ts @@ -2,6 +2,7 @@ import { collectionSignal, liveLinksSignal, pipe, + uniquify, } from '@holochain-open-dev/signals'; import { HashType, LazyHoloHashMap, retype } from '@holochain-open-dev/utils'; import { AgentPubKey } from '@holochain/client'; @@ -11,7 +12,7 @@ import { LinkedDevicesClient } from './linked-devices-client.js'; export class LinkedDevicesStore { constructor(public client: LinkedDevicesClient) { // At startup, clear all the cap grants that might have been left over from an unfinished link agent process - this.client.clearLinkAgent(); + this.client.clearLinkDevices(); } /** Linked Devices for Agent */ @@ -24,7 +25,7 @@ export class LinkedDevicesStore { () => this.client.getLinkedDevicesForAgent(agent), 'AgentToLinkedDevices', ), - links => links.map(l => retype(l.target, HashType.AGENT)), + links => uniquify(links.map(l => retype(l.target, HashType.AGENT))), ), ); diff --git a/zomes/coordinator/linked_devices/src/link_devices.rs b/zomes/coordinator/linked_devices/src/link_devices.rs index d894e1c..5b66f45 100644 --- a/zomes/coordinator/linked_devices/src/link_devices.rs +++ b/zomes/coordinator/linked_devices/src/link_devices.rs @@ -24,18 +24,18 @@ fn secret_from_passcode(passcode: Vec) -> CapSecret { } #[hdk_extern] -pub fn prepare_link_devices(passcode: Vec) -> ExternResult<()> { +pub fn prepare_link_devices(my_passcode: Vec) -> ExternResult<()> { let mut functions = BTreeSet::new(); functions.insert(( zome_info()?.name, - FunctionName("receive_initialize_link_devices".into()), + FunctionName("receive_init_link_devices".into()), )); functions.insert(( zome_info()?.name, FunctionName("receive_request_link_devices".into()), )); let access = CapAccess::Transferable { - secret: secret_from_passcode(passcode), + secret: secret_from_passcode(my_passcode), }; let cap_grant_entry: CapGrantEntry = CapGrantEntry::new( String::from("link-devices"), // A string by which to later query for saved grants. @@ -99,7 +99,7 @@ fn query_link_agents_cap_grants() -> ExternResult> { } #[hdk_extern] -pub fn clear_link_agent() -> ExternResult<()> { +pub fn clear_link_devices() -> ExternResult<()> { let link_agent_cap_grants = query_link_agents_cap_grants()?; for record in link_agent_cap_grants { @@ -170,12 +170,18 @@ pub fn request_link_devices(input: RequestLinkDevicesInput) -> ExternResult<()> timestamp: sys_time()?, }; + let my_signature = sign(my_pub_key.clone(), linked_devices.clone())?; + let incomplete_proof = LinkedDevicesProof { + linked_devices: linked_devices.clone(), + signatures: vec![my_signature.clone()], + }; + let response = call_remote( input.requestor.clone(), zome_info()?.name, "receive_request_link_devices".into(), Some(secret_from_passcode(input.requestor_passcode)), - linked_devices.clone(), + incomplete_proof, )?; let ZomeCallResponse::Ok(result) = response else { @@ -184,7 +190,6 @@ pub fn request_link_devices(input: RequestLinkDevicesInput) -> ExternResult<()> let signature: Signature = result.decode().map_err(|err| wasm_error!(err))?; - let my_signature = sign(my_pub_key.clone(), linked_devices.clone())?; let proof = LinkedDevicesProof { linked_devices, signatures: vec![my_signature, signature], @@ -200,12 +205,8 @@ pub fn request_link_devices(input: RequestLinkDevicesInput) -> ExternResult<()> LinkTypes::AgentToLinkedDevices, tag_bytes.bytes().clone(), )?; - create_link_relaxed( - input.requestor, - my_pub_key, - LinkTypes::AgentToLinkedDevices, - tag_bytes.bytes().clone(), - )?; + + clear_link_devices(())?; Ok(()) } @@ -213,7 +214,10 @@ pub fn request_link_devices(input: RequestLinkDevicesInput) -> ExternResult<()> pub const LINKED_DEVICES_PROOF_TTL_US: u64 = 5_000_000; // 5 seconds #[hdk_extern] -pub fn receive_request_link_devices(linked_devices: LinkedDevices) -> ExternResult { +pub fn receive_request_link_devices( + incomplete_proof: LinkedDevicesProof, +) -> ExternResult { + let linked_devices = incomplete_proof.linked_devices; let my_pub_key = agent_info()?.agent_latest_pubkey; let caller = call_info()?.provenance; @@ -232,7 +236,25 @@ pub fn receive_request_link_devices(linked_devices: LinkedDevices) -> ExternResu )))); } - let my_signature = sign(my_pub_key, linked_devices.clone())?; + let my_signature = sign(my_pub_key.clone(), linked_devices.clone())?; + + let proof = LinkedDevicesProof { + linked_devices, + signatures: vec![incomplete_proof.signatures[0].clone(), my_signature.clone()], + }; + + let tag = AgentToLinkedDevicesLinkTag(vec![proof]); + + let tag_bytes = SerializedBytes::try_from(tag).map_err(|err| wasm_error!(err))?; + + create_link_relaxed( + my_pub_key, + caller, + LinkTypes::AgentToLinkedDevices, + tag_bytes.bytes().clone(), + )?; + + clear_link_devices(())?; Ok(my_signature) } diff --git a/zomes/integrity/linked_devices/src/agent_to_linked_devices.rs b/zomes/integrity/linked_devices/src/agent_to_linked_devices.rs index 1051c2d..53eeb2e 100644 --- a/zomes/integrity/linked_devices/src/agent_to_linked_devices.rs +++ b/zomes/integrity/linked_devices/src/agent_to_linked_devices.rs @@ -33,7 +33,7 @@ fn validate_linked_devices_proof( } pub fn validate_create_link_agent_to_linked_devices( - _action: CreateLink, + action: CreateLink, base_address: AnyLinkableHash, target_address: AnyLinkableHash, tag: LinkTag, @@ -43,6 +43,11 @@ pub fn validate_create_link_agent_to_linked_devices( "No AgentPubKey as the base of an AgentToLinkedDevices link", ))); }; + if base_agent.ne(&action.author) { + return Ok(ValidateCallbackResult::Invalid(String::from( + "Only agents can author AgentToLinkedDevices links using themselves as the base.", + ))); + } let Some(target_agent) = target_address.clone().into_agent_pub_key() else { return Ok(ValidateCallbackResult::Invalid(String::from( "No AgentPubKey as the target of an AgentToLinkedDevices link", diff --git a/zomes/integrity/linked_devices/src/lib.rs b/zomes/integrity/linked_devices/src/lib.rs index bdbc813..d83ce2e 100644 --- a/zomes/integrity/linked_devices/src/lib.rs +++ b/zomes/integrity/linked_devices/src/lib.rs @@ -70,9 +70,20 @@ pub fn validate(op: Op) -> ExternResult { )), _ => Ok(ValidateCallbackResult::Valid), }, - FlatOp::RegisterDelete(delete_entry) => Ok(ValidateCallbackResult::Invalid( - "There are no entry types in this integrity zome".to_string(), - )), + FlatOp::RegisterDelete(delete_entry) => { + let record = must_get_valid_record(delete_entry.action.deletes_address)?; + let Action::Create(create) = record.action() else { + return Ok(ValidateCallbackResult::Invalid( + "There are no entry types in this integrity zome".to_string(), + )); + }; + if let EntryType::CapGrant = create.entry_type { + return Ok(ValidateCallbackResult::Valid); + } + Ok(ValidateCallbackResult::Invalid( + "There are no entry types in this integrity zome".to_string(), + )) + } FlatOp::RegisterCreateLink { link_type, base_address, @@ -118,9 +129,14 @@ pub fn validate(op: Op) -> ExternResult { // Complementary validation to the `StoreEntry` Op, in which the record itself is validated // If you want to optimize performance, you can remove the validation for an entry type here and keep it in `StoreEntry` // Notice that doing so will cause `must_get_valid_record` for this record to return a valid record even if the `StoreEntry` validation failed - OpRecord::CreateEntry { app_entry, action } => Ok(ValidateCallbackResult::Invalid( - "There are no entry types in this integrity zome".to_string(), - )), + OpRecord::CreateEntry { app_entry, action } => { + if let EntryType::CapGrant = action.entry_type { + return Ok(ValidateCallbackResult::Valid); + } + Ok(ValidateCallbackResult::Invalid( + "There are no entry types in this integrity zome".to_string(), + )) + } // Complementary validation to the `RegisterUpdate` Op, in which the record itself is validated // If you want to optimize performance, you can remove the validation for an entry type here and keep it in `StoreEntry` and in `RegisterUpdate` // Notice that doing so will cause `must_get_valid_record` for this record to return a valid record even if the other validations failed @@ -139,9 +155,20 @@ pub fn validate(op: Op) -> ExternResult { original_action_hash, action, .. - } => Ok(ValidateCallbackResult::Invalid( - "There are no entry types in this integrity zome".to_string(), - )), + } => { + let record = must_get_valid_record(original_action_hash)?; + let Action::Create(create) = record.action() else { + return Ok(ValidateCallbackResult::Invalid( + "There are no entry types in this integrity zome".to_string(), + )); + }; + if let EntryType::CapGrant = create.entry_type { + return Ok(ValidateCallbackResult::Valid); + } + Ok(ValidateCallbackResult::Invalid( + "There are no entry types in this integrity zome".to_string(), + )) + } // Complementary validation to the `RegisterCreateLink` Op, in which the record itself is validated // If you want to optimize performance, you can remove the validation for an entry type here and keep it in `RegisterCreateLink` // Notice that doing so will cause `must_get_valid_record` for this record to return a valid record even if the `RegisterCreateLink` validation failed