Optional Slate Message (#2047)

* add optional signed message to slate

* rustfmt
This commit is contained in:
Yeastplume 2018-11-29 12:49:00 +00:00 committed by GitHub
parent b9cfcc777b
commit 16aa64bfab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 185 additions and 43 deletions

View file

@ -268,6 +268,15 @@ Other flags here are:
[host]$ grin wallet send -f -d "http://192.168.0.10:13415" 60.00
```
* `-g` 'Message' - You can specify an optional message to include alongside your transaction data. This message is purely for informational
purposes between all transacting participants, and is not included in transaction data sent to the chain. Each participant message includes
a signature that can be verified with the participant's public key. A message can also be specified by the recipient during a `grin wallet receive`
command.
```sh
[host]$ grin wallet send -f -d "http://192.168.0.10:13415" -g "This is from Dave" 60.00
```
### outputs
Simply displays all the the outputs in your wallet: e.g:
@ -322,7 +331,6 @@ Transaction Log - Account 'default' - Block Height: 49
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
6 Received Tx 03715cf6-f29b-4a3a-bda5-b02cba6bf0d9 2018-07-20 19:46:46.120244904 UTC false None 0 1 60.000000000 0.000000000 None 60.000000000
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>>>>>>> master
To see the inputs/outputs associated with a particular transaction, use the `-i` switch providing the Id of the given transaction, e.g:

View file

@ -352,6 +352,7 @@ impl LocalServerContainer {
max_outputs,
change_outputs,
selection_strategy == "all",
None,
)?;
slate = client_w.send_tx_sync(dest, &slate)?;
api.finalize_tx(&mut slate)?;

View file

@ -969,6 +969,7 @@ fn replicate_tx_fluff_failure() {
500, // max outputs
1000, // num change outputs
true, // select all outputs
None,
)?;
slate = client1_w.send_tx_sync(dest, &slate)?;
api.finalize_tx(&mut slate)?;

View file

@ -355,6 +355,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
e
))
})?;
let message = match send_args.is_present("message") {
true => Some(send_args.value_of("message").unwrap().to_owned()),
false => None,
};
let minimum_confirmations: u64 = send_args
.value_of("minimum_confirmations")
.ok_or_else(|| {
@ -417,6 +421,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
max_outputs,
change_outputs,
selection_strategy == "all",
message,
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
@ -453,11 +458,15 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
slate = adapter.send_tx_sync(dest, &slate)?;
if method == "self" {
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(dest))?;
api.receive_tx(&mut slate, Some(dest), None)?;
Ok(())
})?;
}
api.tx_lock_outputs(&slate, lock_fn)?;
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
api.finalize_tx(&mut slate)?;
} else {
adapter.send_tx_async(dest, &slate)?;
@ -481,6 +490,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
}
("receive", Some(send_args)) => {
let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(());
let message = match send_args.is_present("message") {
true => Some(send_args.value_of("message").unwrap().to_owned()),
false => None,
};
let tx_file = send_args.value_of("input").ok_or_else(|| {
ErrorKind::GenericError("Transaction file required".to_string())
})?;
@ -492,7 +505,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(tx_file)?;
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(account))?;
api.receive_tx(&mut slate, Some(account), message)?;
Ok(())
})?;
let send_tx = format!("{}.response", tx_file);
@ -515,6 +528,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
}
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(tx_file)?;
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
let result = api.post_tx(&slate.tx, fluff);

View file

@ -246,15 +246,25 @@ fn real_main() -> i32 {
.arg(Arg::with_name("fluff")
.help("Fluff the transaction (ignore Dandelion relay protocol)")
.short("f")
.long("fluff")))
.long("fluff"))
.arg(Arg::with_name("message")
.help("Optional participant message to include")
.short("g")
.long("message")
.takes_value(true))
.arg(Arg::with_name("stored_tx")
.help("If present, use the previously stored Unconfirmed transaction with given id.")
.short("t")
.long("stored_tx")
.takes_value(true))
.takes_value(true)))
.subcommand(SubCommand::with_name("receive")
.about("Processes a transaction file to accept a transfer from a sender.")
.arg(Arg::with_name("message")
.help("Optional participant message to include")
.short("g")
.long("message")
.takes_value(true))
.arg(Arg::with_name("input")
.help("Partial transaction to process, expects the sender's transaction file.")
.short("i")

View file

@ -171,7 +171,7 @@ where
}
}
fn retrieve_outputs(
pub fn retrieve_outputs(
&self,
req: &Request<Body>,
api: APIOwner<T, C, K>,
@ -195,7 +195,7 @@ where
api.retrieve_outputs(show_spent, update_from_node, id)
}
fn retrieve_txs(
pub fn retrieve_txs(
&self,
req: &Request<Body>,
api: APIOwner<T, C, K>,
@ -222,7 +222,7 @@ where
api.retrieve_txs(update_from_node, tx_id, tx_slate_id)
}
fn retrieve_stored_tx(
pub fn retrieve_stored_tx(
&self,
req: &Request<Body>,
api: APIOwner<T, C, K>,
@ -251,7 +251,7 @@ where
}
}
fn retrieve_summary_info(
pub fn retrieve_summary_info(
&self,
req: &Request<Body>,
mut api: APIOwner<T, C, K>,
@ -269,7 +269,7 @@ where
api.retrieve_summary_info(update_from_node, minimum_confirmations)
}
fn node_height(
pub fn node_height(
&self,
_req: &Request<Body>,
mut api: APIOwner<T, C, K>,
@ -297,7 +297,7 @@ where
})
}
fn issue_send_tx(
pub fn issue_send_tx(
&self,
req: Request<Body>,
mut api: APIOwner<T, C, K>,
@ -310,6 +310,7 @@ where
args.max_outputs,
args.num_change_outputs,
args.selection_strategy_is_use_all,
args.message,
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
@ -353,7 +354,7 @@ where
}))
}
fn finalize_tx(
pub fn finalize_tx(
&self,
req: Request<Body>,
mut api: APIOwner<T, C, K>,
@ -369,7 +370,7 @@ where
)
}
fn cancel_tx(
pub fn cancel_tx(
&self,
req: Request<Body>,
mut api: APIOwner<T, C, K>,
@ -414,7 +415,7 @@ where
}
}
fn post_tx(
pub fn post_tx(
&self,
req: Request<Body>,
api: APIOwner<T, C, K>,
@ -548,7 +549,8 @@ where
mut api: APIForeign<T, C, K>,
) -> Box<Future<Item = Slate, Error = Error> + Send> {
Box::new(parse_body(req).and_then(
move |mut slate| match api.receive_tx(&mut slate, None) {
//TODO: No way to insert a message from the params
move |mut slate| match api.receive_tx(&mut slate, None, None) {
Ok(_) => ok(slate.clone()),
Err(e) => {
error!("receive_tx: failed with error: {}", e);

View file

@ -420,6 +420,28 @@ pub fn verify_completed_sig(
Ok(())
}
/// Adds signatures
pub fn add_signatures(
secp: &Secp256k1,
part_sigs: Vec<&Signature>,
nonce_sum: &PublicKey,
) -> Result<Signature, Error> {
// Add public nonces kR*G + kS*G
let sig = aggsig::add_signatures_single(&secp, part_sigs, &nonce_sum)?;
Ok(sig)
}
/// Just a simple sig, creates its own nonce, etc
pub fn sign_single(
secp: &Secp256k1,
msg: &Message,
skey: &SecretKey,
pubkey_sum: Option<&PublicKey>,
) -> Result<Signature, Error> {
let sig = aggsig::sign_single(secp, &msg, skey, None, None, None, pubkey_sum, None)?;
Ok(sig)
}
/// Verifies an aggsig signature
pub fn verify_single(
secp: &Secp256k1,
@ -435,17 +457,6 @@ pub fn verify_single(
)
}
/// Adds signatures
pub fn add_signatures(
secp: &Secp256k1,
part_sigs: Vec<&Signature>,
nonce_sum: &PublicKey,
) -> Result<Signature, Error> {
// Add public nonces kR*G + kS*G
let sig = aggsig::add_signatures_single(&secp, part_sigs, &nonce_sum)?;
Ok(sig)
}
/// Just a simple sig, creates its own nonce, etc
pub fn sign_with_blinding(
secp: &Secp256k1,

View file

@ -32,6 +32,8 @@ use util::secp::key::{PublicKey, SecretKey};
use util::secp::Signature;
use util::RwLock;
use blake2::blake2b::blake2b;
/// Public data for each participant in the slate
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -44,6 +46,10 @@ pub struct ParticipantData {
pub public_nonce: PublicKey,
/// Public partial signature
pub part_sig: Option<Signature>,
/// A message for other participants
pub message: Option<String>,
/// Signature, created with private key corresponding to 'public_blind_excess'
pub message_sig: Option<Signature>,
}
impl ParticipantData {
@ -133,6 +139,7 @@ impl Slate {
sec_key: &mut SecretKey,
sec_nonce: &SecretKey,
participant_id: usize,
message: Option<String>,
) -> Result<(), Error>
where
K: Keychain,
@ -141,7 +148,14 @@ impl Slate {
if self.tx.offset == BlindingFactor::zero() {
self.generate_offset(keychain, sec_key)?;
}
self.add_participant_info(keychain, &sec_key, &sec_nonce, participant_id, None)?;
self.add_participant_info(
keychain,
&sec_key,
&sec_nonce,
participant_id,
None,
message,
)?;
Ok(())
}
@ -234,6 +248,7 @@ impl Slate {
sec_nonce: &SecretKey,
id: usize,
part_sig: Option<Signature>,
message: Option<String>,
) -> Result<(), Error>
where
K: Keychain,
@ -241,11 +256,24 @@ impl Slate {
// Add our public key and nonce to the slate
let pub_key = PublicKey::from_secret_key(keychain.secp(), &sec_key)?;
let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?;
// Sign the provided message
let message_sig = {
if let Some(m) = message.clone() {
let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]);
let m = secp::Message::from_slice(&hashed.as_bytes())?;
let res = aggsig::sign_single(&keychain.secp(), &m, &sec_key, None)?;
Some(res)
} else {
None
}
};
self.participant_data.push(ParticipantData {
id: id as u64,
public_blind_excess: pub_key,
public_nonce: pub_nonce,
part_sig: part_sig,
message: message,
message_sig: message_sig,
});
Ok(())
@ -321,6 +349,30 @@ impl Slate {
Ok(())
}
/// Verifies any messages in the slate's participant data match their signatures
pub fn verify_messages(&self, secp: &secp::Secp256k1) -> Result<(), Error> {
for p in self.participant_data.iter() {
if let Some(m) = p.message.clone() {
let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]);
let m = secp::Message::from_slice(&hashed.as_bytes())?;
if !aggsig::verify_single(
secp,
&p.message_sig.as_ref().unwrap(),
&m,
None,
&p.public_blind_excess,
None,
false,
) {
return Err(ErrorKind::Signature(
"Optional participant messages do not match signatures".to_owned(),
))?;
}
}
}
Ok(())
}
/// This should be callable by either the sender or receiver
/// once phase 3 is done
///

View file

@ -462,6 +462,7 @@ where
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
message: Option<String>,
) -> Result<(Slate, impl FnOnce(&mut W, &str) -> Result<(), Error>), Error> {
let mut w = self.wallet.lock();
w.open_with_credentials()?;
@ -485,6 +486,7 @@ where
selection_strategy_is_use_all,
&parent_key_id,
false,
message,
)?;
// Save the aggsig context in our DB for when we
@ -577,6 +579,13 @@ where
}
}
/// Verifies all messages in the slate match their public keys
pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> {
let mut w = self.wallet.lock();
slate.verify_messages(w.keychain().secp())?;
Ok(())
}
/// Attempt to restore contents of wallet
pub fn restore(&mut self) -> Result<(), Error> {
let mut w = self.wallet.lock();
@ -660,6 +669,7 @@ where
&mut self,
slate: &mut Slate,
dest_acct_name: Option<&str>,
message: Option<String>,
) -> Result<(), Error> {
let mut w = self.wallet.lock();
w.open_with_credentials()?;
@ -673,7 +683,7 @@ where
}
None => w.parent_key_id(),
};
let res = tx::receive_tx(&mut *w, slate, &parent_key_id, false);
let res = tx::receive_tx(&mut *w, slate, &parent_key_id, false, message);
w.close()?;
if let Err(e) = res {

View file

@ -31,6 +31,7 @@ pub fn receive_tx<T: ?Sized, C, K>(
slate: &mut Slate,
parent_key_id: &Identifier,
is_self: bool,
message: Option<String>,
) -> Result<(), Error>
where
T: WalletBackend<C, K>,
@ -51,6 +52,7 @@ where
&mut context.sec_key,
&context.sec_nonce,
1,
message,
)?;
// perform partial sig
@ -73,6 +75,7 @@ pub fn create_send_tx<T: ?Sized, C, K>(
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
is_self: bool,
message: Option<String>,
) -> Result<
(
Slate,
@ -122,6 +125,7 @@ where
&mut context.sec_key,
&context.sec_nonce,
0,
message,
)?;
Ok((slate, context, sender_lock_fn))

View file

@ -700,4 +700,6 @@ pub struct SendTXArgs {
pub num_change_outputs: usize,
/// whether to use all outputs (combine)
pub selection_strategy_is_use_all: bool,
/// Optional message, that will be signed
pub message: Option<String>,
}

View file

@ -190,6 +190,7 @@ fn accounts_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;

View file

@ -211,7 +211,7 @@ where
let w = dest_wallet.unwrap().1.clone();
let mut slate = serde_json::from_str(&m.body).unwrap();
controller::foreign_single_use(w.clone(), |listener_api| {
listener_api.receive_tx(&mut slate, None)?;
listener_api.receive_tx(&mut slate, None, None)?;
Ok(())
})?;
Ok(WalletProxyMessage {

View file

@ -97,6 +97,9 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
let send_file = format!("{}/part_tx_1.tx", test_dir);
let receive_file = format!("{}/part_tx_2.tx", test_dir);
// test optional message
let message = "sender test message, sender test message";
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
wallet::controller::owner_single_use(wallet1.clone(), |api| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
@ -106,13 +109,12 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
// send to send
let (mut slate, lock_fn) = api.initiate_tx(
Some("mining"),
reward * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
//"mining",
//"listener",
reward * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
Some(message.to_owned()), // optional message
)?;
// output tx file
let file_adapter = FileWalletCommAdapter::new();
@ -127,11 +129,23 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
w.set_parent_key_id_by_name("account1")?;
}
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&send_file)?;
let mut naughty_slate = slate.clone();
naughty_slate.participant_data[0].message = Some("I changed the message".to_owned());
// verify messages on slate match
wallet::controller::owner_single_use(wallet1.clone(), |api| {
api.verify_slate_messages(&slate)?;
assert!(api.verify_slate_messages(&naughty_slate).is_err());
Ok(())
})?;
let sender2_message = "And this is sender 2's message".to_owned();
// wallet 2 receives file, completes, sends file back
wallet::controller::foreign_single_use(wallet2.clone(), |api| {
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&send_file)?;
api.receive_tx(&mut slate, None)?;
api.receive_tx(&mut slate, None, Some(sender2_message))?;
adapter.send_tx_async(&receive_file, &mut slate)?;
Ok(())
})?;
@ -140,6 +154,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
wallet::controller::owner_single_use(wallet1.clone(), |api| {
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&receive_file)?;
api.verify_slate_messages(&slate)?;
api.finalize_tx(&mut slate)?;
api.post_tx(&slate.tx, false)?;
bh += 1;

View file

@ -112,6 +112,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
// output tx file
let file_adapter = FileWalletCommAdapter::new();
@ -132,7 +133,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
wallet::controller::foreign_single_use(wallet1.clone(), |api| {
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&send_file)?;
api.receive_tx(&mut slate, None)?;
api.receive_tx(&mut slate, None, None)?;
adapter.send_tx_async(&receive_file, &mut slate)?;
Ok(())
})?;
@ -207,6 +208,7 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;

View file

@ -236,6 +236,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;
@ -257,6 +258,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet3", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;
@ -278,6 +280,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client3.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;
@ -305,6 +308,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client3.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;

View file

@ -99,12 +99,11 @@ fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
//"mining",
//"listener",
None,
)?;
// Send directly to self
wallet::controller::foreign_single_use(wallet1.clone(), |api| {
api.receive_tx(&mut slate, Some("listener"))?;
api.receive_tx(&mut slate, Some("listener"), None)?;
Ok(())
})?;
api.finalize_tx(&mut slate)?;

View file

@ -110,6 +110,7 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.tx_lock_outputs(&slate, lock_fn)?;
@ -244,6 +245,7 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.finalize_tx(&mut slate)?;
@ -337,6 +339,7 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> {
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
)?;
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
sender_api.finalize_tx(&mut slate)?;