nym-sdk 1.21.4

Nym's Rust SDK
Documentation
/// Proof-of-concept: two mixnet clients sharing the same identity,
/// where the second client reuses the first client's gateway registration
/// (shared key) without re-registering.
///
/// Could be used in the NS API optimization where
/// multiple gateway probes share a single identity and registration cache.
///
/// Run from the sdk/rust/nym-sdk directory:
///   cargo run --example shared_identity
///
/// Flow:
///   - Client-A registers with a gateway (DH handshake → shared key)
///   - Client-B uses the SAME identity and the SAME shared key
///   - Client-B authenticates (no DH) and gets connected
///   - Both clients get the same Nym address (proves shared identity works)
use nym_client_core::client::base_client::storage::gateways_storage::{
    GatewayDetails, GatewaysDetailsStore, InMemGatewaysDetails,
};
use nym_client_core::client::base_client::storage::{Ephemeral, MixnetClientStorage};
use nym_client_core::client::key_manager::persistence::{InMemEphemeralKeys, KeyStore};
use nym_client_core::client::replies::reply_storage;
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
use nym_credentials_interface::TicketType;
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use rand::rngs::OsRng;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    nym_bin_common::logging::setup_tracing_logger();

    // export ENVIRONMENT=sandbox
    let sandbox_network = mixnet::NymNetworkDetails::new_from_env();

    println!("=== Phase 1: Client-A registers with a gateway ===\n");

    // create client-A with ephemeral storage (will register fresh)
    let storage_a = Ephemeral::default();

    // save a handle to the stores so we can extract data after connection
    let keys_store_a = storage_a.key_store().clone();
    let gw_store_a = storage_a.gateway_details_store().clone();

    let client_a_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_a)
        .network_details(sandbox_network.clone())
        .enable_credentials_mode()
        .build()?;

    let mnemonic = std::env::var("SANDBOX_MNEMONIC").expect("Set mnemonic in the env variable");
    let bandwidth_client_a = client_a_builder
        .create_bandwidth_client(mnemonic, TicketType::V1MixnetEntry)
        .await?;

    // get a bandwidth credential for the mixnet_client
    bandwidth_client_a.acquire().await?;

    // connect: this triggers gateway registration (DH handshake)
    let mut client_a = client_a_builder.connect_to_mixnet().await?;
    let address_a = *client_a.nym_address();
    println!("Client-A address: {address_a}");

    // extract the identity keys and gateway registration from client-A
    let client_keys = keys_store_a.load_keys().await?;
    let active_gw = gw_store_a.active_gateway().await?;
    let registration = active_gw
        .registration
        .expect("client-A should have an active gateway registration");

    // extract the shared key and gateway info
    let shared_key_bytes = registration
        .details
        .shared_key()
        .expect("remote gateway should have a shared key")
        .to_bytes();
    let gateway_id = registration.details.gateway_id();
    let published_data = match &registration.details {
        GatewayDetails::Remote(r) => r.published_data.clone(),
        _ => panic!("expected remote gateway"),
    };

    println!("Gateway ID: {}", gateway_id.to_base58_string());
    println!(
        "Shared key (first 8 bytes): {:02x?}",
        &shared_key_bytes[..8]
    );
    println!("Gateway listener: {}", published_data.listeners.primary);

    // send a self-ping to prove client-A works
    client_a
        .send_plain_message(address_a, "hello from client-A")
        .await?;
    println!("\nClient-A sent self-ping...");
    if let Some(msgs) = client_a.wait_for_messages().await {
        for m in &msgs {
            println!("Client-A received: {}", String::from_utf8_lossy(&m.message));
        }
    }

    // disconnect client-A (don't ForgetMe: keep registration alive)
    client_a.disconnect().await;
    println!("\nClient-A disconnected (gateway keeps registration)\n");

    // ================================================================
    println!("=== Phase 2: Client-B reuses Client-A's identity + registration ===\n");

    // reconstruct the shared key from raw bytes
    let shared_key = SharedSymmetricKey::try_from_bytes(&shared_key_bytes)?;

    // build a pre-populated gateway registration
    let gw_details = GatewayDetails::new_remote(gateway_id, Arc::new(shared_key), published_data);
    let gw_registration = gw_details.into();

    // create a new ephemeral storage and pre-populate it
    let keys_store_b = InMemEphemeralKeys::new(&mut OsRng);
    // inject the SAME identity keys from client-A
    keys_store_b.store_keys(&client_keys).await?;

    let gw_store_b = InMemGatewaysDetails::default();
    // inject the gateway registration (shared key) from client-A
    gw_store_b.store_gateway_details(&gw_registration).await?;
    gw_store_b
        .set_active_gateway(&gateway_id.to_base58_string())
        .await?;

    // wrap into a custom storage that the SDK can use
    let storage_b = PrePopulatedStorage {
        key_store: keys_store_b,
        reply_store: reply_storage::Empty::default(),
        credential_store: EphemeralCredentialStorage::default(),
        gateway_details_store: gw_store_b,
    };

    // Build client-B with the pre-populated storage
    // request_gateway forces the builder to use our pre-registered gateway
    let client_b_builder = mixnet::MixnetClientBuilder::new_with_storage(storage_b)
        .request_gateway(gateway_id.to_base58_string())
        .network_details(sandbox_network)
        .enable_credentials_mode()
        .build()?;

    // === what we'd do when claiming bandwidth: ===
    // but we do NOT do this because we want to reuse registration from client A
    // let bandwidth_client_b = client_b_builder
    //     .create_bandwidth_client(mnemonic, TicketType::V1MixnetEntry)
    //     .await?;
    // bandwidth_client_b.acquire().await?;

    // Connect: this should AUTHENTICATE (not register) because the
    // gateway details store already has a registration for this gateway
    println!("Client-B connecting (should authenticate, not register)...");
    let mut client_b = client_b_builder.connect_to_mixnet().await?;
    let address_b = *client_b.nym_address();
    println!("Client-B address: {address_b}");

    // Verify: both clients get the same Nym address
    if address_a == address_b {
        println!("\n*** SUCCESS: Both clients have the SAME Nym address ***");
        println!("*** Client-B authenticated using Client-A's shared key ***");
    } else {
        println!("\n*** UNEXPECTED: Addresses differ ***");
        println!("  A: {address_a}");
        println!("  B: {address_b}");
    }

    // send a self-ping to prove client-B works
    client_b
        .send_plain_message(address_b, "hello from client-B")
        .await?;
    println!("\nClient-B sent self-ping...");
    if let Some(msgs) = client_b.wait_for_messages().await {
        for m in &msgs {
            println!("Client-B received: {}", String::from_utf8_lossy(&m.message));
        }
    }

    client_b.disconnect().await;
    println!("\nClient-B disconnected. Done!");

    Ok(())
}

// A wrapper around in-memory stores to implement MixnetClientStorage.
// This is the pattern an NS Agent would use to create probes with
// pre-populated identity and gateway registration.
#[derive(Clone)]
struct PrePopulatedStorage {
    key_store: InMemEphemeralKeys,
    reply_store: reply_storage::Empty,
    credential_store: EphemeralCredentialStorage,
    gateway_details_store: InMemGatewaysDetails,
}

impl MixnetClientStorage for PrePopulatedStorage {
    type KeyStore = InMemEphemeralKeys;
    type ReplyStore = reply_storage::Empty;
    type CredentialStore = EphemeralCredentialStorage;
    type GatewaysDetailsStore = InMemGatewaysDetails;

    fn into_runtime_stores(
        self,
    ) -> (
        Self::ReplyStore,
        Self::CredentialStore,
        Self::GatewaysDetailsStore,
    ) {
        (
            self.reply_store,
            self.credential_store,
            self.gateway_details_store,
        )
    }

    fn key_store(&self) -> &Self::KeyStore {
        &self.key_store
    }

    fn reply_store(&self) -> &Self::ReplyStore {
        &self.reply_store
    }

    fn credential_store(&self) -> &Self::CredentialStore {
        &self.credential_store
    }

    fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore {
        &self.gateway_details_store
    }
}