From 42fe2149c749fe831e0772df2a03261d59659c3c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Thu, 4 Jun 2026 15:14:45 -0400 Subject: [PATCH] create_mwixnet_req: take server public keys, not secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MixnetReqCreationParams carried the mix servers' SECRET keys and derived each x25519 onion pubkey from them — a test artifact, since a wallet only knows the servers' public keys. Take the x25519 onion public keys directly. - MixnetReqCreationParams.server_keys (Vec) -> server_pubkeys (Vec) with a vec_xpubkey_hex serde helper (hex list). - create_mwixnet_req uses the pubkeys directly as the onion hop keys. - owner RPC parses hex pubkeys; docs/examples updated; tests derive the pubkeys from their known secrets so coverage is unchanged. - add x25519-dalek to api and controller. --- Cargo.lock | 2 ++ api/Cargo.toml | 1 + api/src/owner.rs | 2 +- api/src/owner_rpc.rs | 31 ++++++++++--------- controller/Cargo.toml | 1 + controller/tests/contract_srs_mwmixnet.rs | 21 ++++++------- controller/tests/mwixnet.rs | 21 ++++++------- libwallet/src/api_impl/owner.rs | 8 +---- libwallet/src/mwixnet/types.rs | 36 +++++++++++++++++++++-- 9 files changed, 76 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f3c588..049cc03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,6 +1510,7 @@ dependencies = [ "serde_json", "tempfile", "uuid", + "x25519-dalek 0.6.0", ] [[package]] @@ -1561,6 +1562,7 @@ dependencies = [ "tokio 0.2.25", "url", "uuid", + "x25519-dalek 0.6.0", ] [[package]] diff --git a/api/Cargo.toml b/api/Cargo.toml index a85781e..e4ea97a 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -21,6 +21,7 @@ chrono = { version = "0.4.11", features = ["serde"] } ring = "0.16" base64 = "0.12" ed25519-dalek = "1.0.0-pre.4" +x25519-dalek = "0.6" grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" } grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } diff --git a/api/src/owner.rs b/api/src/owner.rs index ba2b3ab..53eabb1 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -2543,7 +2543,7 @@ where /// let api_owner = Owner::new(wallet.clone(), None); /// let keychain_mask = None; /// let params = MixnetReqCreationParams { - /// server_keys: vec![], // Public keys here in secret key representation + /// server_pubkeys: vec![], // x25519 onion public keys of the mix servers /// fee_per_hop: 100, /// }; /// diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 32b0266..e2fbbb3 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -2023,10 +2023,10 @@ pub trait OwnerRpc { "commitment": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", "fee_per_hop": "5000000", "lock_output": true, - "server_keys": [ - "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332", - "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e", - "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5" + "server_pubkeys": [ + "24308f58032819d05146db48e78246139f8e30770b1fd1585392df8374d6226a", + "3ab3b7d1fb643393897a0b942934d34be9cb823621cd49c09b12fdfa69598d71", + "80f2ceadcc5a767961772f4ae04826c70b0ae497f4f50d782241ff056b71df45" ] }, "id": 1 @@ -2064,7 +2064,7 @@ pub trait OwnerRpc { commitment: String, fee_per_hop: String, lock_output: bool, - server_keys: Vec, + server_pubkeys: Vec, ) -> Result; } @@ -2541,24 +2541,23 @@ where commitment: String, fee_per_hop: String, lock_output: bool, - server_keys: Vec, + server_pubkeys: Vec, ) -> Result { let commit = Commitment::from_vec(from_hex(&commitment).map_err(|e| Error::CommitDeser(e))?); - let secp_inst = static_secp_instance(); - let secp = secp_inst.lock(); - - let mut keys = vec![]; - for key in server_keys { - keys.push(SecretKey::from_slice( - &secp, - &grin_util::from_hex(&key).map_err(|e| Error::ServerKeyDeser(e))?, - )?) + let mut pubkeys = vec![]; + for key in server_pubkeys { + let bytes = grin_util::from_hex(&key).map_err(|e| Error::ServerKeyDeser(e))?; + let arr: [u8; 32] = bytes + .as_slice() + .try_into() + .map_err(|_| Error::GenericError(format!("x25519 pubkey must be 32 bytes: {}", key)))?; + pubkeys.push(x25519_dalek::PublicKey::from(arr)); } let req_params = MixnetReqCreationParams { - server_keys: keys, + server_pubkeys: pubkeys, fee_per_hop: fee_per_hop .parse::() .map_err(|_| Error::U64Deser(fee_per_hop))?, diff --git a/controller/Cargo.toml b/controller/Cargo.toml index e02f5bb..70adf03 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" [dependencies] ed25519-dalek = "1.0.0-pre.4" +x25519-dalek = "0.6" futures = "0.3" hyper = "0.14" rand = "0.7" diff --git a/controller/tests/contract_srs_mwmixnet.rs b/controller/tests/contract_srs_mwmixnet.rs index 687db00..f816b0a 100644 --- a/controller/tests/contract_srs_mwmixnet.rs +++ b/controller/tests/contract_srs_mwmixnet.rs @@ -102,17 +102,18 @@ fn contract_srs_mwixnet_tx_impl(test_dir: &'static str) -> Result<(), libwallet: "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e"; let server_pubkey_str_3 = "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5"; - let server_key_1 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_1).unwrap()) - .unwrap(); - let server_key_2 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_2).unwrap()) - .unwrap(); - let server_key_3 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_3).unwrap()) - .unwrap(); + // Derive each server's x25519 onion pubkey from the known test secret + // (in practice each server publishes its onion pubkey). + let server_pubkey = |hex: &str| { + let sk = SecretKey::from_slice(&secp, &grin_util::from_hex(hex).unwrap()).unwrap(); + x25519_dalek::PublicKey::from(&x25519_dalek::StaticSecret::from(sk.0)) + }; let params = MixnetReqCreationParams { - server_keys: vec![server_key_1, server_key_2, server_key_3], + server_pubkeys: vec![ + server_pubkey(server_pubkey_str_1), + server_pubkey(server_pubkey_str_2), + server_pubkey(server_pubkey_str_3), + ], fee_per_hop: 50_000_000, }; let outputs = api.retrieve_outputs(recv_mask, false, false, None)?; diff --git a/controller/tests/mwixnet.rs b/controller/tests/mwixnet.rs index 0db537d..b496b20 100644 --- a/controller/tests/mwixnet.rs +++ b/controller/tests/mwixnet.rs @@ -143,17 +143,18 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e"; let server_pubkey_str_3 = "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5"; - let server_key_1 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_1).unwrap()) - .unwrap(); - let server_key_2 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_2).unwrap()) - .unwrap(); - let server_key_3 = - SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_3).unwrap()) - .unwrap(); + // The mwixnet servers' x25519 onion public keys (derived here from known test + // secrets; in practice each server publishes its onion pubkey). + let server_pubkey = |hex: &str| { + let sk = SecretKey::from_slice(&secp, &grin_util::from_hex(hex).unwrap()).unwrap(); + x25519_dalek::PublicKey::from(&x25519_dalek::StaticSecret::from(sk.0)) + }; let params = MixnetReqCreationParams { - server_keys: vec![server_key_1, server_key_2, server_key_3], + server_pubkeys: vec![ + server_pubkey(server_pubkey_str_1), + server_pubkey(server_pubkey_str_2), + server_pubkey(server_pubkey_str_3), + ], fee_per_hop: 50_000_000, }; let outputs = api.retrieve_outputs(mask1, false, false, None)?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 0d5fd0a..f773e0b 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -49,7 +49,6 @@ use chrono::prelude::{DateTime, NaiveDateTime, Utc}; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::Verifier; -use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; use std::convert::{TryFrom, TryInto}; use std::sync::mpsc::Sender; @@ -1665,12 +1664,7 @@ where SwitchCommitmentType::Regular, )?; - let mut server_pubkeys = vec![]; - for i in 0..params.server_keys.len() { - server_pubkeys.push(xPublicKey::from(&StaticSecret::from( - params.server_keys[i].0, - ))); - } + let server_pubkeys = params.server_pubkeys.clone(); let fee = grin_core::libtx::tx_fee(1, 1, 1); let new_amount = amount - (fee * server_pubkeys.len() as u64); diff --git a/libwallet/src/mwixnet/types.rs b/libwallet/src/mwixnet/types.rs index 1213f3a..e741c84 100644 --- a/libwallet/src/mwixnet/types.rs +++ b/libwallet/src/mwixnet/types.rs @@ -17,8 +17,8 @@ use super::onion::comsig_serde; use grin_core::libtx::secp_ser::string_or_u64; -use grin_util::secp::key::SecretKey; use serde::{Deserialize, Serialize}; +use x25519_dalek::PublicKey as xPublicKey; pub use super::onion::{onion::Onion, ComSignature, Hop}; @@ -35,9 +35,39 @@ pub struct SwapReq { /// mwixnetRequest Creation Params #[derive(Serialize, Deserialize, Debug)] pub struct MixnetReqCreationParams { - /// List of all the server keys - pub server_keys: Vec, + /// x25519 onion public keys of the mix servers, in route order + #[serde(with = "vec_xpubkey_hex")] + pub server_pubkeys: Vec, /// Fees per hop #[serde(with = "string_or_u64")] pub fee_per_hop: u64, } + +/// Serializes a Vec as a list of hex strings. +pub mod vec_xpubkey_hex { + use grin_util::{from_hex, ToHex}; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use x25519_dalek::PublicKey as xPublicKey; + + /// + pub fn serialize(keys: &Vec, s: S) -> Result { + let hexes: Vec = keys.iter().map(|k| k.as_bytes().to_hex()).collect(); + serde::Serialize::serialize(&hexes, s) + } + + /// + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let hexes: Vec = Deserialize::deserialize(d)?; + let mut keys = Vec::with_capacity(hexes.len()); + for h in hexes { + let bytes = from_hex(&h).map_err(Error::custom)?; + let arr: [u8; 32] = bytes + .as_slice() + .try_into() + .map_err(|_| Error::custom("x25519 pubkey must be 32 bytes"))?; + keys.push(xPublicKey::from(arr)); + } + Ok(keys) + } +}