diff --git a/Cargo.lock b/Cargo.lock index ccb25d079..70d7e744a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,7 @@ dependencies = [ "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.81 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.81 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index f317bebe1..19f49665d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -30,3 +30,6 @@ chrono = "0.4.4" grin_keychain = { path = "../keychain", version = "1.0.0" } grin_util = { path = "../util", version = "1.0.0" } + +[dev-dependencies] +serde_json = "1" diff --git a/core/src/libtx/mod.rs b/core/src/libtx/mod.rs index 319d85438..a9c802b03 100644 --- a/core/src/libtx/mod.rs +++ b/core/src/libtx/mod.rs @@ -26,7 +26,7 @@ pub mod build; mod error; pub mod proof; pub mod reward; -pub mod serialization; +pub mod secp_ser; pub mod slate; use crate::consensus; diff --git a/core/src/libtx/serialization.rs b/core/src/libtx/secp_ser.rs similarity index 78% rename from core/src/libtx/serialization.rs rename to core/src/libtx/secp_ser.rs index 969be3640..13a4ce14b 100644 --- a/core/src/libtx/serialization.rs +++ b/core/src/libtx/secp_ser.rs @@ -173,3 +173,58 @@ where { serializer.serialize_str(&to_hex(bytes.as_ref().to_vec())) } + +// Test serialization methods of components that are being used +#[cfg(test)] +mod test { + use super::*; + use crate::libtx::aggsig; + use crate::util::secp::key::{PublicKey, SecretKey}; + use crate::util::secp::{Message, Signature}; + use crate::util::static_secp_instance; + + use serde_json; + + use rand::{thread_rng, Rng}; + + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] + struct SerTest { + #[serde(with = "pubkey_serde")] + pub pub_key: PublicKey, + #[serde(with = "option_sig_serde")] + pub opt_sig: Option, + #[serde(with = "sig_serde")] + pub sig: Signature, + } + + impl SerTest { + pub fn random() -> SerTest { + let static_secp = static_secp_instance(); + let secp = static_secp.lock(); + let sk = SecretKey::new(&secp, &mut thread_rng()); + let mut msg = [0u8; 32]; + thread_rng().fill(&mut msg); + let msg = Message::from_slice(&msg).unwrap(); + let sig = aggsig::sign_single(&secp, &msg, &sk, None).unwrap(); + SerTest { + pub_key: PublicKey::from_secret_key(&secp, &sk).unwrap(), + opt_sig: Some(sig.clone()), + sig: sig.clone(), + } + } + } + + #[test] + fn ser_secp_primitives() { + for _ in 0..10 { + let s = SerTest::random(); + println!("Before Serialization: {:?}", s); + let serialized = serde_json::to_string_pretty(&s).unwrap(); + println!("JSON: {}", serialized); + let deserialized: SerTest = serde_json::from_str(&serialized).unwrap(); + println!("After Serialization: {:?}", deserialized); + println!(); + assert_eq!(s, deserialized); + } + } +} diff --git a/core/src/libtx/slate.rs b/core/src/libtx/slate.rs index c1ab36c60..2fc2fe3b7 100644 --- a/core/src/libtx/slate.rs +++ b/core/src/libtx/slate.rs @@ -22,7 +22,7 @@ use crate::core::transaction::{kernel_features, kernel_sig_msg, Transaction, Wei use crate::core::verifier_cache::LruVerifierCache; use crate::keychain::{BlindSum, BlindingFactor, Keychain}; use crate::libtx::error::{Error, ErrorKind}; -use crate::libtx::{aggsig, build, tx_fee}; +use crate::libtx::{aggsig, build, secp_ser, tx_fee}; use crate::util::secp; use crate::util::secp::key::{PublicKey, SecretKey}; use crate::util::secp::Signature; @@ -66,6 +66,33 @@ impl ParticipantData { } } +/// Public message data (for serialising and storage) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantMessageData { + /// id of the particpant in the tx + pub id: u64, + /// Public key + #[serde(with = "secp_ser::pubkey_serde")] + pub public_key: PublicKey, + /// Message, + pub message: Option, + /// Signature + #[serde(with = "secp_ser::option_sig_serde")] + pub message_sig: Option, +} + +impl ParticipantMessageData { + /// extract relevant message data from participant data + pub fn from_participant_data(p: &ParticipantData) -> ParticipantMessageData { + ParticipantMessageData { + id: p.id, + public_key: p.public_blind_excess, + message: p.message.clone(), + message_sig: p.message_sig.clone(), + } + } +} + /// A 'Slate' is passed around to all parties to build up all of the public /// transaction data needed to create a finalized transaction. Callers can pass /// the slate around by whatever means they choose, (but we can provide some @@ -94,6 +121,13 @@ pub struct Slate { pub participant_data: Vec, } +/// Helper just to facilitate serialization +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantMessages { + /// included messages + pub messages: Vec, +} + impl Slate { /// Create a new slate pub fn blank(num_participants: usize) -> Slate { @@ -274,10 +308,19 @@ impl Slate { message: message, message_sig: message_sig, }); - Ok(()) } + /// helper to return all participant messages + pub fn participant_messages(&self) -> ParticipantMessages { + let mut ret = ParticipantMessages { messages: vec![] }; + for ref m in self.participant_data.iter() { + ret.messages + .push(ParticipantMessageData::from_participant_data(m)); + } + ret + } + /// Somebody involved needs to generate an offset with their private key /// For now, we'll have the transaction initiator be responsible for it /// Return offset private key for the participant to use later in the diff --git a/src/bin/cmd/wallet_tests.rs b/src/bin/cmd/wallet_tests.rs index f9b301fe4..6aa6eff7d 100644 --- a/src/bin/cmd/wallet_tests.rs +++ b/src/bin/cmd/wallet_tests.rs @@ -253,6 +253,16 @@ mod wallet_tests { let mut bh = 10u64; let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + let very_long_message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + This part should all be truncated"; + // Update info and check let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "info"]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; @@ -273,7 +283,7 @@ mod wallet_tests { "-d", &file_name, "-g", - "Love, Yeast", + very_long_message, "10", ]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; @@ -491,6 +501,12 @@ mod wallet_tests { let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "txs"]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + // message output (mostly spit out for a visual in test logs) + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "-a", "mining", "txs", "-i", "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + // txs and outputs (mostly spit out for a visual in test logs) let arg_vec = vec![ "grin", "wallet", "-p", "password", "-a", "mining", "outputs", diff --git a/wallet/src/command.rs b/wallet/src/command.rs index c45925dba..35e51959f 100644 --- a/wallet/src/command.rs +++ b/wallet/src/command.rs @@ -396,15 +396,19 @@ pub fn txs( &g_args.account, height, validated, - txs, + &txs, include_status, dark_scheme, )?; // if given a particular transaction id, also get and display associated - // inputs/outputs + // inputs/outputs and messages if args.id.is_some() { let (_, outputs) = api.retrieve_outputs(true, false, args.id)?; display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?; + // should only be one here, but just in case + for tx in txs { + display::tx_messages(&tx, dark_scheme)?; + } }; Ok(()) })?; diff --git a/wallet/src/display.rs b/wallet/src/display.rs index b66965110..73d0bf2d5 100644 --- a/wallet/src/display.rs +++ b/wallet/src/display.rs @@ -123,7 +123,7 @@ pub fn txs( account: &str, cur_height: u64, validated: bool, - txs: Vec, + txs: &Vec, include_status: bool, dark_background_color_scheme: bool, ) -> Result<(), Error> { @@ -357,3 +357,77 @@ pub fn accounts(acct_mappings: Vec) { table.printstd(); println!(); } + +/// Display transaction log messages +pub fn tx_messages(tx: &TxLogEntry, dark_background_color_scheme: bool) -> Result<(), Error> { + let title = format!("Transaction Messages - Transaction '{}'", tx.id,); + println!(); + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let msgs = match tx.messages.clone() { + None => { + writeln!(t, "{}", "None").unwrap(); + t.reset().unwrap(); + return Ok(()); + } + Some(m) => m.clone(), + }; + + if msgs.messages.is_empty() { + writeln!(t, "{}", "None").unwrap(); + t.reset().unwrap(); + return Ok(()); + } + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Participant Id", + bMG->"Message", + bMG->"Public Key", + bMG->"Signature", + ]); + + let secp = util::static_secp_instance(); + let secp_lock = secp.lock(); + + for m in msgs.messages { + let id = format!("{}", m.id); + let public_key = format!( + "{}", + util::to_hex(m.public_key.serialize_vec(&secp_lock, true).to_vec()) + ); + let message = match m.message { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let message_sig = match m.message_sig { + Some(s) => format!("{}", util::to_hex(s.serialize_der(&secp_lock))), + None => "None".to_owned(), + }; + if dark_background_color_scheme { + table.add_row(row![ + bFC->id, + bFC->message, + bFC->public_key, + bFB->message_sig, + ]); + } else { + table.add_row(row![ + bFD->id, + bFb->message, + bFD->public_key, + bFB->message_sig, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + Ok(()) +} diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 9d247a148..337582fe0 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -48,6 +48,8 @@ use crate::libwallet::{Error, ErrorKind}; use crate::util; use crate::util::secp::{pedersen, ContextFlag, Secp256k1}; +const USER_MESSAGE_MAX_LEN: usize = 256; + /// Functions intended for use by the owner (e.g. master seed holder) of the wallet. pub struct APIOwner where @@ -551,7 +553,8 @@ where /// ParticipantData within the slate. This message will include a signature created with the /// sender's private keys, and will be publically verifiable. Note this message is for /// the convenience of the participants during the exchange; it is not included in the final - /// transaction sent to the chain. Validation of this message is optional. + /// transaction sent to the chain. The message will be truncated to 256 characters. + /// Validation of this message is optional. /// /// # Returns /// * a result containing: @@ -640,6 +643,14 @@ where None => w.parent_key_id(), }; + let message = match message { + Some(mut m) => { + m.truncate(USER_MESSAGE_MAX_LEN); + Some(m) + } + None => None, + }; + let (slate, context, lock_fn) = tx::create_send_tx( &mut *w, amount, @@ -685,12 +696,12 @@ where let context = w.get_private_context(slate.id.as_bytes())?; tx::complete_tx(&mut *w, slate, &context)?; tx::update_stored_tx(&mut *w, slate)?; + tx::update_message(&mut *w, slate)?; { let mut batch = w.batch()?; batch.delete_private_context(slate.id.as_bytes())?; batch.commit()?; } - w.close()?; Ok(()) } @@ -873,6 +884,15 @@ where return Err(ErrorKind::TransactionAlreadyReceived(slate.id.to_string()).into()); } } + + let message = match message { + Some(mut m) => { + m.truncate(USER_MESSAGE_MAX_LEN); + Some(m) + } + None => None, + }; + let res = tx::receive_tx(&mut *w, slate, &parent_key_id, message); w.close()?; diff --git a/wallet/src/libwallet/internal/selection.rs b/wallet/src/libwallet/internal/selection.rs index fcc6ed4ca..560aa616e 100644 --- a/wallet/src/libwallet/internal/selection.rs +++ b/wallet/src/libwallet/internal/selection.rs @@ -100,6 +100,7 @@ where let lock_inputs = context.get_inputs().clone(); let _lock_outputs = context.get_outputs().clone(); + let messages = Some(slate.participant_messages()); // Return a closure to acquire wallet lock and lock the coins being spent // so we avoid accidental double spend attempt. @@ -122,6 +123,7 @@ where } t.amount_debited = amount_debited; + t.messages = messages; // write the output representing our change for (change_amount, id, _) in &change_amounts_derivations { @@ -195,6 +197,7 @@ where ); context.add_output(&key_id, &None); + let messages = Some(slate.participant_messages()); // Create closure that adds the output to recipient's wallet // (up to the caller to decide when to do) @@ -206,6 +209,7 @@ where t.tx_slate_id = Some(slate_id); t.amount_credited = amount; t.num_outputs = 1; + t.messages = messages; batch.save(OutputData { root_key_id: parent_key_id.clone(), key_id: key_id_inner.clone(), diff --git a/wallet/src/libwallet/internal/tx.rs b/wallet/src/libwallet/internal/tx.rs index c68dd3084..2bfb42244 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -39,7 +39,6 @@ where // create an output using the amount in the slate let (_, mut context, receiver_create_fn) = selection::build_recipient_output_with_slate(wallet, slate, parent_key_id.clone())?; - // fill public keys let _ = slate.fill_round_1( wallet.keychain(), @@ -55,6 +54,8 @@ where // Save output in wallet let _ = receiver_create_fn(wallet); + update_message(wallet, slate)?; + Ok(()) } @@ -224,6 +225,26 @@ where Ok(()) } +/// Update the transaction participant messages +pub fn update_message(wallet: &mut T, slate: &Slate) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, false)?; + if tx_vec.is_empty() { + return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?; + } + let mut batch = wallet.batch()?; + for mut tx in tx_vec.into_iter() { + tx.messages = Some(slate.participant_messages()); + let parent_key = tx.parent_key_id.clone(); + batch.save_tx_log_entry(tx, &parent_key)?; + } + batch.commit()?; + Ok(()) +} #[cfg(test)] mod test { use crate::core::libtx::build; diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index cf2a35278..0b8523c3c 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -25,6 +25,7 @@ use crate::util::secp::key::{PublicKey, SecretKey}; use crate::util::secp::{self, pedersen, Secp256k1}; use chrono::prelude::*; use failure::ResultExt; +use grin_core::libtx::slate::ParticipantMessages; use serde; use serde_json; use std::collections::HashMap; @@ -610,6 +611,8 @@ pub struct TxLogEntry { pub amount_debited: u64, /// Fee pub fee: Option, + /// Message data, stored as json + pub messages: Option, /// Location of the store transaction, (reference or resending) pub stored_tx: Option, } @@ -643,6 +646,7 @@ impl TxLogEntry { num_inputs: 0, num_outputs: 0, fee: None, + messages: None, stored_tx: None, } } diff --git a/wallet/tests/file.rs b/wallet/tests/file.rs index c4202f730..925f7cca4 100644 --- a/wallet/tests/file.rs +++ b/wallet/tests/file.rs @@ -27,6 +27,8 @@ use std::fs; use std::thread; use std::time::Duration; +use serde_json; + fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); } @@ -137,7 +139,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { // wallet 2 receives file, completes, sends file back wallet::controller::foreign_single_use(wallet2.clone(), |api| { - api.receive_tx(&mut slate, None, Some(sender2_message))?; + api.receive_tx(&mut slate, None, Some(sender2_message.clone()))?; adapter.send_tx_async(&receive_file, &mut slate)?; Ok(()) })?; @@ -174,6 +176,36 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { Ok(()) })?; + // Check messages, all participants should have both + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?; + assert_eq!( + tx[0].clone().messages.unwrap().messages[0].message, + Some(message.to_owned()) + ); + assert_eq!( + tx[0].clone().messages.unwrap().messages[1].message, + Some(sender2_message.to_owned()) + ); + + let msg_json = serde_json::to_string_pretty(&tx[0].clone().messages.unwrap()).unwrap(); + println!("{}", msg_json); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?; + assert_eq!( + tx[0].clone().messages.unwrap().messages[0].message, + Some(message.to_owned()) + ); + assert_eq!( + tx[0].clone().messages.unwrap().messages[1].message, + Some(sender2_message) + ); + Ok(()) + })?; + // let logging finish thread::sleep(Duration::from_millis(200)); Ok(())