Save slate participant messages in database (#2441)

* Save and display optional slate messages

* rustfmt

* fixes and test updates

* rustfmt

* output json

* rustfmt

* add better serialisation of slate message, rename to secp_ser, add tests for serialization functions

* rustfmt

* max length for message enforced by API

* rustfmt
This commit is contained in:
Yeastplume 2019-01-31 11:55:34 +00:00 committed by GitHub
parent 422db82667
commit 0c851c5140
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 288 additions and 11 deletions

1
Cargo.lock generated
View file

@ -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)",
]

View file

@ -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"

View file

@ -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;

View file

@ -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<Signature>,
#[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);
}
}
}

View file

@ -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<String>,
/// Signature
#[serde(with = "secp_ser::option_sig_serde")]
pub message_sig: Option<Signature>,
}
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<ParticipantData>,
}
/// Helper just to facilitate serialization
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ParticipantMessages {
/// included messages
pub messages: Vec<ParticipantMessageData>,
}
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

View file

@ -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",

View file

@ -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(())
})?;

View file

@ -123,7 +123,7 @@ pub fn txs(
account: &str,
cur_height: u64,
validated: bool,
txs: Vec<TxLogEntry>,
txs: &Vec<TxLogEntry>,
include_status: bool,
dark_background_color_scheme: bool,
) -> Result<(), Error> {
@ -357,3 +357,77 @@ pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
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(())
}

View file

@ -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<W: ?Sized, C, K>
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()?;

View file

@ -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(),

View file

@ -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<T: ?Sized, C, K>(wallet: &mut T, slate: &Slate) -> Result<(), Error>
where
T: WalletBackend<C, K>,
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;

View file

@ -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<u64>,
/// Message data, stored as json
pub messages: Option<ParticipantMessages>,
/// Location of the store transaction, (reference or resending)
pub stored_tx: Option<String>,
}
@ -643,6 +646,7 @@ impl TxLogEntry {
num_inputs: 0,
num_outputs: 0,
fee: None,
messages: None,
stored_tx: None,
}
}

View file

@ -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(())