create_mwixnet_req: take server public keys, not secrets

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<SecretKey>) -> server_pubkeys
  (Vec<x25519 PublicKey>) 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.
This commit is contained in:
jwinterm
2026-06-04 15:14:45 -04:00
parent ce779188b1
commit 42fe2149c7
9 changed files with 76 additions and 47 deletions
Generated
+2
View File
@@ -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]]
+1
View File
@@ -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" }
+1 -1
View File
@@ -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,
/// };
///
+15 -16
View File
@@ -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<String>,
server_pubkeys: Vec<String>,
) -> Result<SwapReq, Error>;
}
@@ -2541,24 +2541,23 @@ where
commitment: String,
fee_per_hop: String,
lock_output: bool,
server_keys: Vec<String>,
server_pubkeys: Vec<String>,
) -> Result<SwapReq, Error> {
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::<u64>()
.map_err(|_| Error::U64Deser(fee_per_hop))?,
+1
View File
@@ -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"
+11 -10
View File
@@ -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)?;
+11 -10
View File
@@ -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)?;
+1 -7
View File
@@ -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);
+33 -3
View File
@@ -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<SecretKey>,
/// x25519 onion public keys of the mix servers, in route order
#[serde(with = "vec_xpubkey_hex")]
pub server_pubkeys: Vec<xPublicKey>,
/// Fees per hop
#[serde(with = "string_or_u64")]
pub fee_per_hop: u64,
}
/// Serializes a Vec<x25519 PublicKey> 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<S: Serializer>(keys: &Vec<xPublicKey>, s: S) -> Result<S::Ok, S::Error> {
let hexes: Vec<String> = 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<Vec<xPublicKey>, D::Error> {
let hexes: Vec<String> = 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)
}
}