From 7293ca99c374fe9499ef1d93eba9128c373d53b0 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Thu, 28 Nov 2019 14:34:27 +0000 Subject: [PATCH] Payment Proof Implementation (#259) * refactor address generation code into libwallet, bool to flag whether to include proof, add sender address in init_send_tx * rustfmt * require payment proof addr as part of init_tx * rustfmt * store payment proof on sender transaction side * rustfmt * change sig to ed25519 sig * rustfmt * add message creation and signature * rustfmt * add payment proof verification function * rustfmt * validate proof on sender side, store proof * rustfmt * fix json tests * fixes and updates to tests * added API functions for converting and retrieving proof addresses * rustfmt * add payment proof to init_send_tx example * rustfmt * incorrect comment --- Cargo.lock | 5 +- api/Cargo.toml | 1 + api/src/lib.rs | 4 +- api/src/owner.rs | 106 ++++++++++- api/src/owner_rpc.rs | 6 + api/src/owner_rpc_s.rs | 97 +++++++++- api/src/types.rs | 11 ++ controller/src/controller.rs | 4 +- controller/tests/payment_proofs.rs | 153 ++++++++++++++++ controller/tests/updater_thread.rs | 12 +- impls/Cargo.toml | 1 - impls/src/error.rs | 4 +- impls/src/tor/config.rs | 105 ++--------- libwallet/Cargo.toml | 3 + libwallet/src/address.rs | 156 ++++++++++++++++ libwallet/src/api_impl/foreign.rs | 19 +- libwallet/src/api_impl/owner.rs | 53 +++++- libwallet/src/api_impl/types.rs | 7 + libwallet/src/error.rs | 12 ++ libwallet/src/internal/selection.rs | 17 ++ libwallet/src/internal/tx.rs | 217 ++++++++++++++++++++++- libwallet/src/lib.rs | 6 +- libwallet/src/slate.rs | 9 +- libwallet/src/slate_versions/ser.rs | 54 ++++++ libwallet/src/slate_versions/v3.rs | 9 +- libwallet/src/types.rs | 40 +++++ tests/data/v2_reqs/init_send_tx.req.json | 1 + tests/data/v3_reqs/init_send_tx.req.json | 1 + 28 files changed, 988 insertions(+), 125 deletions(-) create mode 100644 controller/tests/payment_proofs.rs create mode 100644 libwallet/src/address.rs diff --git a/Cargo.lock b/Cargo.lock index 84588241..46d0b7b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -918,6 +918,7 @@ dependencies = [ "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "easy-jsonrpc-mw 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ed25519-dalek 1.0.0-pre.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "grin_wallet_config 3.0.0-alpha.1", @@ -1005,7 +1006,6 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "sysinfo 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", "timer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1021,7 +1021,9 @@ name = "grin_wallet_libwallet" version = "3.0.0-alpha.1" dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "data-encoding 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "ed25519-dalek 1.0.0-pre.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1033,6 +1035,7 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "strum_macros 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/api/Cargo.toml b/api/Cargo.toml index c3d8cf48..a56bee68 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -22,6 +22,7 @@ easy-jsonrpc-mw = "0.5.3" chrono = { version = "0.4.4", features = ["serde"] } ring = "0.13" base64 = "0.9" +ed25519-dalek = "1.0.0-pre.1" grin_wallet_libwallet = { path = "../libwallet", version = "3.0.0-alpha.1" } grin_wallet_config = { path = "../config", version = "3.0.0-alpha.1" } diff --git a/api/src/lib.rs b/api/src/lib.rs index 10c3d8d4..0dc91b2a 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -56,4 +56,6 @@ pub use crate::foreign_rpc::foreign_rpc as foreign_rpc_client; pub use crate::foreign_rpc::run_doctest_foreign; pub use crate::owner_rpc::run_doctest_owner; -pub use types::{ECDHPubkey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Token}; +pub use types::{ + ECDHPubkey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, PubAddress, Token, +}; diff --git a/api/src/owner.rs b/api/src/owner.rs index b830bced..87794f90 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -15,6 +15,7 @@ //! Owner API External Definition use chrono::prelude::*; +use ed25519_dalek::PublicKey as DalekPublicKey; use uuid::Uuid; use crate::config::{TorConfig, WalletConfig}; @@ -25,7 +26,7 @@ use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::{ - AcctPathMapping, Error, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, + address, AcctPathMapping, Error, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, Slate, TxLogEntry, WalletInfo, WalletInst, WalletLCProvider, }; @@ -1886,6 +1887,109 @@ where let index = q.len().saturating_sub(count); Ok(q.split_off(index)) } + + /// Retrieve the public proof "addresses" associated with the active account at the + /// given derivation path. + /// + /// In this case, an "address" means a Dalek ed25519 public key corresponding to + /// a private key derived as follows: + /// + /// e.g. The default parent account is at + /// + /// `m/0/0` + /// + /// With output blinding factors created as + /// + /// `m/0/0/0` + /// `m/0/0/1` etc... + /// + /// The corresponding public address derivation path would be at: + /// + /// `m/0/1` + /// + /// With addresses created as: + /// + /// `m/0/1/0` + /// `m/0/1/1` etc... + /// + /// Note that these addresses correspond to the public keys used in the addresses + /// of TOR hidden services configured by the wallet listener. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// * `derivation_index` - The index along the derivation path to retrieve an address for + /// + /// # Returns + /// * Ok with a DalekPublicKey representing the address + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let res = api_owner.get_public_proof_address(None, 0); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// ``` + + pub fn get_public_proof_address( + &self, + keychain_mask: Option<&SecretKey>, + derivation_index: u32, + ) -> Result { + owner::get_public_proof_address(self.wallet_inst.clone(), keychain_mask, derivation_index) + } + + /// Helper function to convert an Onion v3 address to a payment proof address (essentially + /// exctacting and verifying the public key) + /// + /// # Arguments + /// + /// * `address_v3` - An V3 Onion address + /// + /// # Returns + /// * Ok(DalekPublicKey) representing the public key associated with the address, if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// or the address provided is invalid + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let res = api_owner.proof_address_from_onion_v3( + /// "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid" + /// ); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// let res = api_owner.stop_updater(); + /// ``` + + pub fn proof_address_from_onion_v3(&self, address_v3: &str) -> Result { + address::pubkey_from_onion_v3(address_v3) + } } #[doc(hidden)] diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 35717b5f..5edb4a52 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -240,6 +240,7 @@ pub trait OwnerRpc: Sync + Send { "parent_key_id": "0200000000000000000000000000000000", "stored_tx": null, "tx_slate_id": null, + "payment_proof": null, "tx_type": "ConfirmedCoinbase" }, { @@ -258,6 +259,7 @@ pub trait OwnerRpc: Sync + Send { "parent_key_id": "0200000000000000000000000000000000", "stored_tx": null, "tx_slate_id": null, + "payment_proof": null, "tx_type": "ConfirmedCoinbase" } ] @@ -340,6 +342,7 @@ pub trait OwnerRpc: Sync + Send { "selection_strategy_is_use_all": true, "message": "my message", "target_slate_version": null, + "payment_proof_recipient_address": null, "send_args": null } }, @@ -561,6 +564,7 @@ pub trait OwnerRpc: Sync + Send { "selection_strategy_is_use_all": true, "message": "Ok, here are your grins", "target_slate_version": null, + "payment_proof_recipient_address": null, "send_args": null } ], @@ -1485,6 +1489,8 @@ pub fn run_doctest_owner( assert!(wallet_refreshed); } + //let proof_address = api_impl::owner::get_public_proof_address(wallet2.clone(), (&mask2).as_ref(), 0).unwrap(); + if perform_tx { let amount = 60_000_000_000; let mut w_lock = wallet1.lock(); diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index 6ef663c3..f127a8dd 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -27,7 +27,7 @@ use crate::libwallet::{ }; use crate::util::secp::key::{PublicKey, SecretKey}; use crate::util::{static_secp_instance, LoggingConfig, ZeroingString}; -use crate::{ECDHPubkey, Owner, Token}; +use crate::{ECDHPubkey, Owner, PubAddress, Token}; use easy_jsonrpc_mw; use rand::thread_rng; use std::time::Duration; @@ -264,6 +264,7 @@ pub trait OwnerRpcS { "parent_key_id": "0200000000000000000000000000000000", "stored_tx": null, "tx_slate_id": null, + "payment_proof": null, "tx_type": "ConfirmedCoinbase" }, { @@ -281,6 +282,7 @@ pub trait OwnerRpcS { "num_outputs": 1, "parent_key_id": "0200000000000000000000000000000000", "stored_tx": null, + "payment_proof": null, "tx_slate_id": null, "tx_type": "ConfirmedCoinbase" } @@ -371,6 +373,7 @@ pub trait OwnerRpcS { "selection_strategy_is_use_all": true, "message": "my message", "target_slate_version": null, + "payment_proof_recipient_address": "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb", "send_args": null } }, @@ -391,7 +394,11 @@ pub trait OwnerRpcS { "lock_height": "0", "ttl_cutoff_height": "0", "num_participants": 2, - "payment_proof": null, + "payment_proof": { + "receiver_address": "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb", + "receiver_signature": null, + "sender_address": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, "participant_data": [ { "id": "0", @@ -598,6 +605,7 @@ pub trait OwnerRpcS { "selection_strategy_is_use_all": true, "message": "Ok, here are your grins", "target_slate_version": null, + "payment_proof_recipient_address": null, "send_args": null } }, @@ -1776,6 +1784,71 @@ pub trait OwnerRpcS { */ fn get_updater_messages(&self, count: u32) -> Result, ErrorKind>; + + /** + Networked version of [Owner::get_public_proof_address](struct.Owner.html#method.get_public_proof_address). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_public_proof_address", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "derivation_index": 0 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn get_public_proof_address( + &self, + token: Token, + derivation_index: u32, + ) -> Result; + + /** + Networked version of [Owner::proof_address_from_onion_v3](struct.Owner.html#method.proof_address_from_onion_v3). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "proof_address_from_onion_v3", + "params": { + "address_v3": "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb" + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn proof_address_from_onion_v3(&self, address_v3: String) -> Result; } impl OwnerRpcS for Owner @@ -2080,4 +2153,24 @@ where fn get_updater_messages(&self, count: u32) -> Result, ErrorKind> { Owner::get_updater_messages(self, count as usize).map_err(|e| e.kind()) } + + fn get_public_proof_address( + &self, + token: Token, + derivation_index: u32, + ) -> Result { + let address = Owner::get_public_proof_address( + self, + (&token.keychain_mask).as_ref(), + derivation_index, + ) + .map_err(|e| e.kind())?; + Ok(PubAddress { address }) + } + + fn proof_address_from_onion_v3(&self, address_v3: String) -> Result { + let address = + Owner::proof_address_from_onion_v3(self, &address_v3).map_err(|e| e.kind())?; + Ok(PubAddress { address }) + } } diff --git a/api/src/types.rs b/api/src/types.rs index 2cd53c77..ae47c064 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -12,12 +12,14 @@ // limitations under the License. use crate::core::libtx::secp_ser; +use crate::libwallet::dalek_ser; use crate::libwallet::{Error, ErrorKind}; use crate::util::secp::key::{PublicKey, SecretKey}; use crate::util::{from_hex, to_hex}; use failure::ResultExt; use base64; +use ed25519_dalek::PublicKey as DalekPublicKey; use rand::{thread_rng, Rng}; use ring::aead; use serde_json::{self, Value}; @@ -32,6 +34,15 @@ pub struct Token { pub keychain_mask: Option, } +/// Wrapper for dalek public keys, used as addresses +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct PubAddress { + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + /// Public address + pub address: DalekPublicKey, +} + /// Wrapper for ECDH Public keys #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(transparent)] diff --git a/controller/src/controller.rs b/controller/src/controller.rs index bb23dc95..1c7632ab 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -17,7 +17,7 @@ use crate::api::{self, ApiServer, BasicAuthMiddleware, ResponseFuture, Router, TLSConfig}; use crate::keychain::Keychain; use crate::libwallet::{ - Error, ErrorKind, NodeClient, NodeVersionInfo, Slate, WalletInst, WalletLCProvider, + address, Error, ErrorKind, NodeClient, NodeVersionInfo, Slate, WalletInst, WalletLCProvider, CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION, }; use crate::util::secp::key::SecretKey; @@ -98,7 +98,7 @@ where let k = w_inst.keychain((&mask).as_ref())?; let parent_key_id = w_inst.parent_key_id(); let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?); - let sec_key = tor_config::address_derivation_path(&k, &parent_key_id, 0) + let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; let onion_address = tor_config::onion_address_from_seckey(&sec_key) .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs new file mode 100644 index 00000000..a59010dc --- /dev/null +++ b/controller/tests/payment_proofs.rs @@ -0,0 +1,153 @@ +// Copyright 2019 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_util; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, Slate}; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Various tests on accounts within the same wallet +fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + + // Do some mining + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let mut address = None; + wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + address = Some(api.get_public_proof_address(m, 0)?); + Ok(()) + })?; + + let address = address.unwrap(); + println!("Public address is: {:?}", address); + let amount = 60_000_000_000; + let mut slate = Slate::blank(1); + wallet::controller::owner_single_use(wallet1.clone(), mask1, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: Some(address), + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + + assert_eq!( + slate_i.payment_proof.as_ref().unwrap().receiver_address, + address + ); + println!( + "Sender addr: {:?}", + slate_i.payment_proof.as_ref().unwrap().sender_address + ); + + // Check we are creating a tx with the expected lock_height of 0. + // We will check this produces a Plain kernel later. + assert_eq!(0, slate.lock_height); + + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate, 0)?; + + // Ensure what's stored in TX log for payment proof is correct + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + assert!(txs[0].payment_proof.is_some()); + let pp = txs[0].clone().payment_proof.unwrap(); + assert_eq!( + pp.receiver_address, + slate_i.payment_proof.as_ref().unwrap().receiver_address + ); + assert!(pp.receiver_signature.is_some()); + assert_eq!(pp.sender_address_path, 0); + assert_eq!(pp.sender_signature, None); + + slate = sender_api.finalize_tx(m, &slate)?; + + // Check payment proof here + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id))?; + let tx = txs[0].clone(); + + println!("{:?}", tx); + + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn payment_proofs() { + let test_dir = "test_output/payment_proofs"; + setup(test_dir); + if let Err(e) = payment_proofs_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/updater_thread.rs b/controller/tests/updater_thread.rs index bec1921b..33b85ffe 100644 --- a/controller/tests/updater_thread.rs +++ b/controller/tests/updater_thread.rs @@ -19,13 +19,10 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; -use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; -use grin_wallet_util::grin_core as core; +// use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; +// use grin_wallet_util::grin_core as core; -use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient}; -use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; -use std::sync::mpsc::channel; use std::thread; use std::time::Duration; @@ -71,9 +68,6 @@ fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err } }); - // few values to keep things shorter - let reward = core::consensus::REWARD; - // add some accounts wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| { api.create_account_path(m, "mining")?; @@ -93,7 +87,7 @@ fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err wallet_inst!(wallet1, w); w.set_parent_key_id_by_name("mining")?; } - let mut bh = 10u64; + let bh = 10u64; let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); diff --git a/impls/Cargo.toml b/impls/Cargo.toml index 54295dff..94ac6a13 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -42,7 +42,6 @@ tokio-io = "0.1" #tokio-tls = "0.1" ed25519-dalek = "1.0.0-pre.1" data-encoding = "2" -sha3 = "0.8" regex = "1.3" timer = "0.2" sysinfo = "0.9" diff --git a/impls/src/error.rs b/impls/src/error.rs index 00f7e4e9..abead3f1 100644 --- a/impls/src/error.rs +++ b/impls/src/error.rs @@ -83,8 +83,8 @@ pub enum ErrorKind { ED25519Key(String), /// Checking for onion address - #[fail(display = "Address is not an Onion v3 Address")] - NotOnion, + #[fail(display = "Address is not an Onion v3 Address: {}", _0)] + NotOnion(String), /// Other #[fail(display = "Generic error: {}", _0)] diff --git a/impls/src/tor/config.rs b/impls/src/tor/config.rs index dd0e74ec..2c0c399e 100644 --- a/impls/src/tor/config.rs +++ b/impls/src/tor/config.rs @@ -15,15 +15,11 @@ //! Tor Configuration + Onion (Hidden) Service operations use crate::util::secp::key::SecretKey; use crate::{Error, ErrorKind}; -use grin_wallet_util::grin_keychain::{ChildNumber, Identifier, Keychain, SwitchCommitmentType}; +use grin_wallet_libwallet::address; -use data_encoding::BASE32; use ed25519_dalek::ExpandedSecretKey; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; -use sha3::{Digest, Sha3_256}; - -use crate::blake2::blake2b::blake2b; use std::fs::{self, File}; use std::io::Write; @@ -96,40 +92,10 @@ impl TorRcConfig { } } -/// Output ed25519 keypair given an rust_secp256k1 SecretKey -pub fn ed25519_keypair(sec_key: &SecretKey) -> Result<(DalekSecretKey, DalekPublicKey), Error> { - let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { - Ok(k) => k, - Err(e) => { - return Err(ErrorKind::ED25519Key(format!("{}", e)).to_owned())?; - } - }; - let d_pub_key: DalekPublicKey = (&d_skey).into(); - Ok((d_skey, d_pub_key)) -} - /// helper to get address pub fn onion_address_from_seckey(sec_key: &SecretKey) -> Result { - let (_, d_pub_key) = ed25519_keypair(sec_key)?; - onion_address(&d_pub_key) -} - -/// Generate an onion address from an ed25519_dalek public key -pub fn onion_address(pub_key: &DalekPublicKey) -> Result { - // calculate checksum - let mut hasher = Sha3_256::new(); - hasher.input(b".onion checksum"); - hasher.input(pub_key.as_bytes()); - hasher.input([0x03u8]); - let checksum = hasher.result(); - - let mut address_bytes = pub_key.as_bytes().to_vec(); - address_bytes.push(checksum[0]); - address_bytes.push(checksum[1]); - address_bytes.push(0x03u8); - - let ret = BASE32.encode(&address_bytes); - Ok(ret.to_lowercase()) + let (_, d_pub_key) = address::ed25519_keypair(sec_key)?; + Ok(address::onion_v3_from_pubkey(&d_pub_key)?) } pub fn create_onion_service_sec_key_file( @@ -178,8 +144,8 @@ pub fn output_onion_service_config( tor_config_directory: &str, sec_key: &SecretKey, ) -> Result { - let (_, d_pub_key) = ed25519_keypair(&sec_key)?; - let address = onion_address(&d_pub_key)?; + let (_, d_pub_key) = address::ed25519_keypair(&sec_key)?; + let address = address::onion_v3_from_pubkey(&d_pub_key)?; let hs_dir_file_path = format!( "{}{}{}{}{}", tor_config_directory, MAIN_SEPARATOR, HIDDEN_SERVICES_DIR, MAIN_SEPARATOR, address @@ -193,7 +159,7 @@ pub fn output_onion_service_config( // create directory if it doesn't exist fs::create_dir_all(&hs_dir_file_path).context(ErrorKind::IO)?; - let (d_sec_key, d_pub_key) = ed25519_keypair(&sec_key)?; + let (d_sec_key, d_pub_key) = address::ed25519_keypair(&sec_key)?; create_onion_service_sec_key_file(&hs_dir_file_path, &d_sec_key)?; create_onion_service_pub_key_file(&hs_dir_file_path, &d_pub_key)?; create_onion_service_hostname_file(&hs_dir_file_path, &address)?; @@ -272,51 +238,14 @@ pub fn output_tor_sender_config( Ok(()) } -/// Derive a secret key given a derivation path and index -pub fn address_derivation_path( - keychain: &K, - parent_key_id: &Identifier, - index: u32, -) -> Result -where - K: Keychain, -{ - let mut key_path = parent_key_id.to_path(); - // An output derivation for acct m/0 - // is m/0/0/0, m/0/0/1 (for instance), m/1 is m/1/0/0, m/1/0/1 - // Address generation path should be - // for m/0: m/0/1/0, m/0/1/1 - // for m/1: m/1/1/0, m/1/1/1 - key_path.path[1] = ChildNumber::from(1); - key_path.depth = key_path.depth + 1; - key_path.path[key_path.depth as usize - 1] = ChildNumber::from(index); - let key_id = Identifier::from_path(&key_path); - debug!("Onion Address derivation path is: {}", key_id); - let sec_key = keychain.derive_key(0, &key_id, &SwitchCommitmentType::None)?; - let hashed = blake2b(32, &[], &sec_key.0[..]); - Ok(SecretKey::from_slice( - &keychain.secp(), - &hashed.as_bytes()[..], - )?) -} - pub fn is_tor_address(input: &str) -> Result<(), Error> { - let mut input = input.to_uppercase(); - if input.starts_with("HTTP://") || input.starts_with("HTTPS://") { - input = input.replace("HTTP://", ""); - input = input.replace("HTTPS://", ""); + match address::pubkey_from_onion_v3(input) { + Ok(_) => Ok(()), + Err(e) => { + let msg = format!("{}", e); + Err(ErrorKind::NotOnion(msg).to_owned())? + } } - if input.ends_with(".ONION") { - input = input.replace(".ONION", ""); - } - // for now, just check input is the right length and is base32 - if input.len() != 56 { - return Err(ErrorKind::NotOnion.to_owned())?; - } - let _ = BASE32 - .decode(input.as_bytes()) - .context(ErrorKind::NotOnion)?; - Ok(()) } pub fn complete_tor_address(input: &str) -> Result { @@ -356,12 +285,12 @@ mod tests { let mut test_rng = StepRng::new(1234567890u64, 1); let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); println!("{:?}", sec_key); - let (_, d_pub_key) = ed25519_keypair(&sec_key)?; + let (_, d_pub_key) = address::ed25519_keypair(&sec_key)?; println!("{:?}", d_pub_key); // some randoms for _ in 0..1000 { let sec_key = secp::key::SecretKey::new(&secp, &mut thread_rng()); - let (_, _) = ed25519_keypair(&sec_key)?; + let (_, _) = address::ed25519_keypair(&sec_key)?; } Ok(()) } @@ -373,8 +302,8 @@ mod tests { let mut test_rng = StepRng::new(1234567890u64, 1); let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); println!("{:?}", sec_key); - let (_, d_pub_key) = ed25519_keypair(&sec_key)?; - let address = onion_address(&d_pub_key)?; + let (_, d_pub_key) = address::ed25519_keypair(&sec_key)?; + let address = address::onion_v3_from_pubkey(&d_pub_key)?; assert_eq!( "kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid", address @@ -412,6 +341,8 @@ mod tests { #[test] fn test_is_tor_address() -> Result<(), Error> { assert!(is_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid").is_ok()); + assert!(is_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid").is_ok()); + assert!(is_tor_address("kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid").is_ok()); assert!(is_tor_address( "http://kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid.onion" ) diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 8bb1f98e..d122863b 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -25,6 +25,9 @@ lazy_static = "1" strum = "0.15" strum_macros = "0.15" ed25519-dalek = "1.0.0-pre.1" +sha3 = "0.8" +byteorder = "1" +data-encoding = "2" grin_wallet_util = { path = "../util", version = "3.0.0-alpha.1" } grin_wallet_config = { path = "../config", version = "3.0.0-alpha.1" } diff --git a/libwallet/src/address.rs b/libwallet/src/address.rs new file mode 100644 index 00000000..99c4a037 --- /dev/null +++ b/libwallet/src/address.rs @@ -0,0 +1,156 @@ +// Copyright 2019 The Grin Develope; +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functions defining wallet 'addresses', i.e. ed2559 keys based on +//! a derivation path + +use crate::grin_util::secp::key::SecretKey; +use crate::{Error, ErrorKind}; +use grin_wallet_util::grin_keychain::{ChildNumber, Identifier, Keychain, SwitchCommitmentType}; + +use data_encoding::BASE32; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use failure::ResultExt; +use sha3::{Digest, Sha3_256}; + +use crate::blake2::blake2b::blake2b; + +/// Derive a secret key given a derivation path and index +pub fn address_from_derivation_path( + keychain: &K, + parent_key_id: &Identifier, + index: u32, +) -> Result +where + K: Keychain, +{ + let mut key_path = parent_key_id.to_path(); + // An output derivation for acct m/0 + // is m/0/0/0, m/0/0/1 (for instance), m/1 is m/1/0/0, m/1/0/1 + // Address generation path should be + // for m/0: m/0/1/0, m/0/1/1 + // for m/1: m/1/1/0, m/1/1/1 + key_path.path[1] = ChildNumber::from(1); + key_path.depth = key_path.depth + 1; + key_path.path[key_path.depth as usize - 1] = ChildNumber::from(index); + let key_id = Identifier::from_path(&key_path); + let sec_key = keychain.derive_key(0, &key_id, &SwitchCommitmentType::None)?; + let hashed = blake2b(32, &[], &sec_key.0[..]); + Ok(SecretKey::from_slice( + &keychain.secp(), + &hashed.as_bytes()[..], + )?) +} + +/// Output ed25519 keypair given an rust_secp256k1 SecretKey +pub fn ed25519_keypair(sec_key: &SecretKey) -> Result<(DalekSecretKey, DalekPublicKey), Error> { + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(ErrorKind::ED25519Key(format!("{}", e)).to_owned())?; + } + }; + let d_pub_key: DalekPublicKey = (&d_skey).into(); + Ok((d_skey, d_pub_key)) +} + +/// Return the ed25519 public key represented in an onion address +pub fn pubkey_from_onion_v3(onion_address: &str) -> Result { + let mut input = onion_address.to_uppercase(); + if input.starts_with("HTTP://") || input.starts_with("HTTPS://") { + input = input.replace("HTTP://", ""); + input = input.replace("HTTPS://", ""); + } + if input.ends_with(".ONION") { + input = input.replace(".ONION", ""); + } + let orig_address_raw = input.clone(); + // for now, just check input is the right length and try and decode from base32 + if input.len() != 56 { + return Err( + ErrorKind::AddressDecoding("Input address is wrong length".to_owned()).to_owned(), + )?; + } + let mut address = BASE32 + .decode(input.as_bytes()) + .context(ErrorKind::AddressDecoding( + "Input address is not base 32".to_owned(), + ))? + .to_vec(); + + address.split_off(32); + let key = match DalekPublicKey::from_bytes(&address) { + Ok(k) => k, + Err(_) => { + return Err(ErrorKind::AddressDecoding( + "Provided onion V3 address is invalid (parsing key)".to_owned(), + ) + .to_owned())?; + } + }; + let test_v3 = match onion_v3_from_pubkey(&key) { + Ok(k) => k, + Err(_) => { + return Err(ErrorKind::AddressDecoding( + "Provided onion V3 address is invalid (converting from pubkey)".to_owned(), + ) + .to_owned())?; + } + }; + + if test_v3.to_uppercase() != orig_address_raw.to_uppercase() { + return Err(ErrorKind::AddressDecoding( + "Provided onion V3 address is invalid (no match)".to_owned(), + ) + .to_owned())?; + } + Ok(key) +} + +/// Generate an onion address from an ed25519_dalek public key +pub fn onion_v3_from_pubkey(pub_key: &DalekPublicKey) -> Result { + // calculate checksum + let mut hasher = Sha3_256::new(); + hasher.input(b".onion checksum"); + hasher.input(pub_key.as_bytes()); + hasher.input([0x03u8]); + let checksum = hasher.result(); + + let mut address_bytes = pub_key.as_bytes().to_vec(); + address_bytes.push(checksum[0]); + address_bytes.push(checksum[1]); + address_bytes.push(0x03u8); + + let ret = BASE32.encode(&address_bytes); + Ok(ret.to_lowercase()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn onion_v3_conversion() { + let onion_address = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid"; + + let key = pubkey_from_onion_v3(onion_address).unwrap(); + println!("Key: {:?}", &key); + + let out_address = onion_v3_from_pubkey(&key).unwrap(); + println!("Address: {:?}", &out_address); + + assert_eq!(onion_address, out_address); + } +} diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 8a056f45..365501bf 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -20,7 +20,7 @@ use crate::grin_util::secp::key::SecretKey; use crate::internal::{tx, updater}; use crate::slate_versions::SlateVersion; use crate::{ - BlockFees, CbData, Error, ErrorKind, NodeClient, Slate, TxLogEntryType, VersionInfo, + address, BlockFees, CbData, Error, ErrorKind, NodeClient, Slate, TxLogEntryType, VersionInfo, WalletBackend, }; @@ -113,6 +113,21 @@ where use_test_rng, )?; tx::update_message(&mut *w, keychain_mask, &mut ret_slate)?; + + let keychain = w.keychain(keychain_mask)?; + let excess = ret_slate.calc_excess(&keychain)?; + + if let Some(ref mut p) = ret_slate.payment_proof { + let sig = tx::create_payment_proof_signature( + ret_slate.amount, + &excess, + p.sender_address, + address::address_from_derivation_path(&keychain, &parent_key_id, 0)?, + )?; + + p.receiver_signature = Some(sig); + } + Ok(ret_slate) } @@ -130,7 +145,7 @@ where let mut sl = slate.clone(); let context = w.get_private_context(keychain_mask, sl.id.as_bytes(), 1)?; tx::complete_tx(&mut *w, keychain_mask, &mut sl, 1, &context)?; - tx::update_stored_tx(&mut *w, keychain_mask, &mut sl, true)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &mut sl, true)?; tx::update_message(&mut *w, keychain_mask, &mut sl)?; { let mut batch = w.batch(keychain_mask)?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index d42c58f5..f3ba30a3 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -26,13 +26,15 @@ use crate::grin_util::Mutex; use crate::api_impl::owner_updater::StatusMessage; use crate::grin_keychain::{Identifier, Keychain}; use crate::internal::{keys, scan, selection, tx, updater}; -use crate::slate::Slate; +use crate::slate::{PaymentInfo, Slate}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, TxWrapper, WalletBackend, WalletInfo}; use crate::{ - wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, + address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, ScannedBlockInfo, TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, }; use crate::{Error, ErrorKind}; +use ed25519_dalek::PublicKey as DalekPublicKey; + use std::sync::mpsc::Sender; use std::sync::Arc; @@ -72,6 +74,26 @@ where w.set_parent_key_id_by_name(label) } +/// Retrieve the payment proof address for the current parent key at +/// the given index +/// set active account +pub fn get_public_proof_address<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + index: u32, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + let k = w.keychain(keychain_mask)?; + let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, index)?; + Ok(address::ed25519_keypair(&sec_addr_key)?.1) +} + /// retrieve outputs pub fn retrieve_outputs<'a, L, C, K>( wallet_inst: Arc>>>, @@ -222,7 +244,7 @@ where return Ok(slate); } - let context = tx::add_inputs_to_slate( + let mut context = tx::add_inputs_to_slate( &mut *w, keychain_mask, &mut slate, @@ -237,6 +259,26 @@ where use_test_rng, )?; + // Payment Proof, add addresses to slate and save address + // TODO: Note we only use single derivation path for now, + // probably want to allow sender to specify which one + let deriv_path = 0u32; + + if let Some(a) = args.payment_proof_recipient_address { + let k = w.keychain(keychain_mask)?; + + let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, deriv_path)?; + let sender_address = address::ed25519_keypair(&sec_addr_key)?.1; + + slate.payment_proof = Some(PaymentInfo { + sender_address, + receiver_address: a, + receiver_signature: None, + }); + + context.payment_proof_derivation_index = Some(deriv_path); + } + // Save the aggsig context in our DB for when we // recieve the transaction back { @@ -247,6 +289,7 @@ where if let Some(v) = args.target_slate_version { slate.version_info.orig_version = v; } + Ok(slate) } @@ -417,8 +460,10 @@ where { let mut sl = slate.clone(); let context = w.get_private_context(keychain_mask, sl.id.as_bytes(), 0)?; + let parent_key_id = w.parent_key_id(); tx::complete_tx(&mut *w, keychain_mask, &mut sl, 0, &context)?; - tx::update_stored_tx(&mut *w, keychain_mask, &mut sl, false)?; + tx::verify_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &mut sl, false)?; tx::update_message(&mut *w, keychain_mask, &mut sl)?; { let mut batch = w.batch(keychain_mask)?; diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 8d212374..053c7839 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -17,9 +17,12 @@ use crate::grin_core::libtx::secp_ser; use crate::grin_keychain::Identifier; use crate::grin_util::secp::pedersen; +use crate::slate_versions::ser as dalek_ser; use crate::slate_versions::SlateVersion; use crate::types::OutputData; +use ed25519_dalek::PublicKey as DalekPublicKey; + /// Send TX API Args // TODO: This is here to ensure the legacy V1 API remains intact // remove this when v1 api is removed @@ -86,6 +89,9 @@ pub struct InitTxArgs { /// down to the minimum slate version compatible with the current. If `None` the slate /// is generated with the latest version. pub target_slate_version: Option, + /// If set, require a payment proof for the particular recipient + #[serde(with = "dalek_ser::option_dalek_pubkey_serde")] + pub payment_proof_recipient_address: Option, /// If true, just return an estimate of the resulting slate, containing fees and amounts /// locked without actually locking outputs or creating the transaction. Note if this is set to /// 'true', the amount field in the slate will contain the total amount locked, not the provided @@ -124,6 +130,7 @@ impl Default for InitTxArgs { message: None, target_slate_version: None, estimate_only: Some(false), + payment_proof_recipient_address: None, send_args: None, } } diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index a4366f0f..f62795f5 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -221,6 +221,18 @@ pub enum ErrorKind { #[fail(display = "Tor Config Error: {}", _0)] TorConfig(String), + /// Generating ED25519 Public Key + #[fail(display = "Error generating ed25519 secret key: {}", _0)] + ED25519Key(String), + + /// Generating Payment Proof + #[fail(display = "Payment Proof generation error: {}", _0)] + PaymentProof(String), + + /// Decoding OnionV3 addresses to payment proof addresses + #[fail(display = "Proof Address decoding: {}", _0)] + AddressDecoding(String), + /// Other #[fail(display = "Generic error: {}", _0)] GenericError(String), diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 012672b7..30f97f8e 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -160,6 +160,23 @@ where t.amount_debited = amount_debited; t.messages = messages; + // store extra payment proof info, if required + if let Some(ref p) = slate.payment_proof { + t.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address.clone(), + receiver_signature: p.receiver_signature.clone(), + sender_address_path: match context.payment_proof_derivation_index { + Some(p) => p, + None => { + return Err(ErrorKind::PaymentProof( + "Payment proof derivation index required".to_owned(), + ))?; + } + }, + sender_signature: None, + }); + }; + // write the output representing our change for (id, _, _) in &context.get_outputs() { t.num_outputs += 1; diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 96f2601f..8bb7204e 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -14,17 +14,24 @@ //! Transaction building functions +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::io::Cursor; use uuid::Uuid; use crate::grin_core::consensus::valid_header_version; use crate::grin_core::core::HeaderVersion; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen; use crate::grin_util::Mutex; use crate::internal::{selection, updater}; use crate::slate::Slate; -use crate::types::{Context, NodeClient, TxLogEntryType, WalletBackend}; -use crate::{Error, ErrorKind}; +use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; +use crate::{address, Error, ErrorKind}; +use ed25519_dalek::Keypair as DalekKeypair; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Signature as DalekSignature; // static for incrementing test UUIDs lazy_static! { @@ -303,6 +310,7 @@ where pub fn update_stored_tx<'a, T: ?Sized, C, K>( wallet: &mut T, keychain_mask: Option<&SecretKey>, + context: &Context, slate: &Slate, is_invoiced: bool, ) -> Result<(), Error> @@ -332,6 +340,29 @@ where wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), &slate.tx)?; let parent_key = tx.parent_key_id.clone(); tx.kernel_excess = Some(slate.tx.body.kernels[0].excess); + + if let Some(ref p) = slate.payment_proof { + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = wallet.parent_key_id(); + let excess = slate.calc_excess(&keychain)?; + let sig = create_payment_proof_signature( + slate.amount, + &excess, + p.sender_address, + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?, + )?; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.receiver_signature, + sender_address_path: derivation_index, + sender_signature: Some(sig), + }) + } + let mut batch = wallet.batch(keychain_mask)?; batch.save_tx_log_entry(tx, &parent_key)?; batch.commit()?; @@ -363,11 +394,146 @@ where Ok(()) } +pub fn payment_proof_message( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, +) -> Result, Error> { + let mut msg = Vec::new(); + msg.write_u64::(amount)?; + msg.append(&mut kernel_commitment.0.to_vec()); + msg.append(&mut sender_address.to_bytes().to_vec()); + Ok(msg) +} + +pub fn _decode_payment_proof_message( + msg: &Vec, +) -> Result<(u64, pedersen::Commitment, DalekPublicKey), Error> { + let mut rdr = Cursor::new(msg); + let amount = rdr.read_u64::()?; + let mut commit_bytes = [0u8; 33]; + for i in 0..33 { + commit_bytes[i] = rdr.read_u8()?; + } + let mut sender_address_bytes = [0u8; 32]; + for i in 0..32 { + sender_address_bytes[i] = rdr.read_u8()?; + } + + Ok(( + amount, + pedersen::Commitment::from_vec(commit_bytes.to_vec()), + DalekPublicKey::from_bytes(&sender_address_bytes).unwrap(), + )) +} + +/// create a payment proof +pub fn create_payment_proof_signature( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, + sec_key: SecretKey, +) -> Result { + let msg = payment_proof_message(amount, kernel_commitment, sender_address)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(ErrorKind::ED25519Key(format!("{}", e)).to_owned())?; + } + }; + let pub_key: DalekPublicKey = (&d_skey).into(); + let keypair = DalekKeypair { + public: pub_key, + secret: d_skey, + }; + Ok(keypair.sign(&msg)) +} + +/// Verify all aspects of a completed payment proof +pub fn verify_payment_proof<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + context: &Context, + slate: &Slate, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if let Some(ref p) = slate.payment_proof { + let keychain = wallet.keychain(keychain_mask)?; + let index = match context.payment_proof_derivation_index { + Some(i) => i, + None => { + return Err(ErrorKind::PaymentProof( + "Payment proof derivation index required".to_owned(), + ))?; + } + }; + let orig_sender_sk = + address::address_from_derivation_path(&keychain, parent_key_id, index)?; + let orig_sender_address = address::ed25519_keypair(&orig_sender_sk)?.1; + if p.sender_address != orig_sender_address { + return Err(ErrorKind::PaymentProof( + "Sender address on slate does not match original sender address".to_owned(), + ))?; + } + let tx_vec = + updater::retrieve_txs(wallet, None, Some(slate.id), Some(&parent_key_id), false)?; + if tx_vec.len() != 1 { + return Err(ErrorKind::PaymentProof( + "TxLogEntry with original proof info not found".to_owned(), + ))?; + } + let orig_proof_info = match tx_vec[0].clone().payment_proof { + Some(o) => o, + None => { + return Err(ErrorKind::PaymentProof( + "Original proof info not stored in tx".to_owned(), + ))?; + } + }; + if orig_proof_info.receiver_address != p.receiver_address { + return Err(ErrorKind::PaymentProof( + "Recipient address on slate does not match original recipient address".to_owned(), + ))?; + } + let msg = payment_proof_message( + slate.amount, + &slate.calc_excess(&keychain)?, + orig_sender_address, + )?; + let sig = match p.receiver_signature { + Some(s) => s, + None => { + return Err(ErrorKind::PaymentProof( + "Recipient did not provide requested proof signature".to_owned(), + ))?; + } + }; + + if let Err(_) = p.receiver_address.verify(&msg, &sig) { + return Err(ErrorKind::PaymentProof( + "Invalid proof signature".to_owned(), + ))?; + }; + } + Ok(()) +} + #[cfg(test)] mod test { + use super::*; + use rand::rngs::mock::StepRng; + use crate::grin_core::core::KernelFeatures; use crate::grin_core::libtx::{build, ProofBuilder}; - use crate::grin_keychain::{ExtKeychain, ExtKeychainPath, Keychain}; + use crate::grin_keychain::{ + BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Keychain, SwitchCommitmentType, + }; + use crate::grin_util::{secp, static_secp_instance}; #[test] // demonstrate that input.commitment == referenced output.commitment @@ -395,4 +561,49 @@ mod test { assert_eq!(tx1.outputs()[0].features, tx2.inputs()[0].features); assert_eq!(tx1.outputs()[0].commitment(), tx2.inputs()[0].commitment()); } + + #[test] + fn payment_proof_construction() { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let mut test_rng = StepRng::new(1234567890u64, 1); + let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); + let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); + + let address: DalekPublicKey = (&d_skey).into(); + + let kernel_excess = { + ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = &SwitchCommitmentType::Regular; + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)), + ) + .unwrap(); + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let amount = 12345678u64; + let msg = payment_proof_message(amount, &kernel_excess, address).unwrap(); + println!("payment proof message is (len {}): {:?}", msg.len(), msg); + + let decoded = _decode_payment_proof_message(&msg).unwrap(); + assert_eq!(decoded.0, amount); + assert_eq!(decoded.1, kernel_excess); + assert_eq!(decoded.2, address); + + let sig = create_payment_proof_signature(amount, &kernel_excess, address, sec_key).unwrap(); + + assert!(address.verify(&msg, &sig).is_ok()); + } } diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 14b01876..2eeb93c7 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -44,6 +44,7 @@ extern crate strum; #[macro_use] extern crate strum_macros; +pub mod address; pub mod api_impl; mod error; mod internal; @@ -63,10 +64,11 @@ pub use api_impl::types::{ OutputCommitMapping, SendTXArgs, VersionInfo, }; pub use internal::scan::scan; +pub use slate_versions::ser as dalek_ser; pub use types::{ AcctPathMapping, BlockIdentifier, CbData, Context, NodeClient, NodeVersionInfo, OutputData, - OutputStatus, ScannedBlockInfo, TxLogEntry, TxLogEntryType, TxWrapper, WalletBackend, - WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, WalletOutputBatch, + OutputStatus, ScannedBlockInfo, StoredProofInfo, TxLogEntry, TxLogEntryType, TxWrapper, + WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, WalletOutputBatch, }; /// Helper for taking a lock on the wallet instance diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index e84e2bfd..bb676b26 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -32,6 +32,7 @@ use crate::grin_util::secp::Signature; use crate::grin_util::{self, secp, RwLock}; use crate::slate_versions::ser as dalek_ser; use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; use failure::ResultExt; use rand::rngs::mock::StepRng; use rand::thread_rng; @@ -52,10 +53,10 @@ use crate::types::CbData; pub struct PaymentInfo { #[serde(with = "dalek_ser::dalek_pubkey_serde")] pub sender_address: DalekPublicKey, - #[serde(with = "dalek_ser::option_dalek_pubkey_serde")] - pub receiver_address: Option, - #[serde(with = "secp_ser::option_sig_serde")] - pub receiver_signature: Option, + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub receiver_address: DalekPublicKey, + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub receiver_signature: Option, } /// Public data for each participant in the slate diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 7b54cddb..147e6520 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -80,6 +80,46 @@ pub mod option_dalek_pubkey_serde { }) } } + +/// Serializes an Option to and from hex +pub mod option_dalek_sig_serde { + use ed25519_dalek::Signature as DalekSignature; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::grin_util::{from_hex, to_hex}; + + /// + pub fn serialize(key: &Option, serializer: S) -> Result + where + S: Serializer, + { + match key { + Some(key) => serializer.serialize_str(&to_hex(key.to_bytes().to_vec())), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => from_hex(string.to_string()) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 64]; + b.copy_from_slice(&bytes[0..64]); + DalekSignature::from_bytes(&b) + .map(|val| Some(val)) + .map_err(|err| Error::custom(err.to_string())) + }), + None => Ok(None), + }) + } +} + // Test serialization methods of components that are being used #[cfg(test)] mod test { @@ -87,8 +127,10 @@ mod test { use rand::rngs::mock::StepRng; use crate::grin_util::{secp, static_secp_instance}; + use ed25519_dalek::Keypair; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; + use ed25519_dalek::Signature as DalekSignature; use serde::Deserialize; use serde_json; @@ -99,6 +141,8 @@ mod test { pub pub_key: DalekPublicKey, #[serde(with = "option_dalek_pubkey_serde")] pub pub_key_opt: Option, + #[serde(with = "option_dalek_sig_serde")] + pub sig_opt: Option, } impl SerTest { @@ -109,9 +153,19 @@ mod test { let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); let d_pub_key: DalekPublicKey = (&d_skey).into(); + + let keypair = Keypair { + public: d_pub_key, + secret: d_skey, + }; + + let d_sig = keypair.sign("test sig".as_bytes()); + println!("D sig: {:?}", d_sig); + SerTest { pub_key: d_pub_key.clone(), pub_key_opt: Some(d_pub_key), + sig_opt: Some(d_sig), } } } diff --git a/libwallet/src/slate_versions/v3.rs b/libwallet/src/slate_versions/v3.rs index b158162b..48c9ec93 100644 --- a/libwallet/src/slate_versions/v3.rs +++ b/libwallet/src/slate_versions/v3.rs @@ -27,6 +27,7 @@ use crate::grin_util::secp::Signature; use crate::slate::CompatKernelFeatures; use crate::slate_versions::ser as dalek_ser; use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; use uuid::Uuid; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -105,10 +106,10 @@ pub struct ParticipantDataV3 { pub struct PaymentInfoV3 { #[serde(with = "dalek_ser::dalek_pubkey_serde")] pub sender_address: DalekPublicKey, - #[serde(with = "dalek_ser::option_dalek_pubkey_serde")] - pub receiver_address: Option, - #[serde(with = "secp_ser::option_sig_serde")] - pub receiver_signature: Option, + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub receiver_address: DalekPublicKey, + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub receiver_signature: Option, } /// A transaction diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 6444f723..d9a613c9 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -26,7 +26,10 @@ use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::{self, pedersen, Secp256k1}; use crate::grin_util::{LoggingConfig, ZeroingString}; use crate::slate::ParticipantMessages; +use crate::slate_versions::ser as dalek_ser; use chrono::prelude::*; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; use failure::ResultExt; use serde; use serde_json; @@ -546,6 +549,8 @@ pub struct Context { pub fee: u64, /// keep track of the participant id pub participant_id: usize, + /// Payment proof sender address derivation path, if needed + pub payment_proof_derivation_index: Option, } impl Context { @@ -569,6 +574,7 @@ impl Context { output_ids: vec![], fee: 0, participant_id: participant_id, + payment_proof_derivation_index: None, } } } @@ -784,6 +790,9 @@ pub struct TxLogEntry { /// of kernel is necessary #[serde(default)] pub kernel_lookup_min_height: Option, + /// Additional info needed to stored payment proof + #[serde(default)] + pub payment_proof: Option, } impl ser::Writeable for TxLogEntry { @@ -819,6 +828,7 @@ impl TxLogEntry { stored_tx: None, kernel_excess: None, kernel_lookup_min_height: None, + payment_proof: None, } } @@ -836,6 +846,36 @@ impl TxLogEntry { } } +/// Payment proof information. Differs from what is sent via +/// the slate +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StoredProofInfo { + /// receiver address + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub receiver_address: DalekPublicKey, + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + /// receiver signature + pub receiver_signature: Option, + /// sender address derivation path index + pub sender_address_path: u32, + /// sender signature + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub sender_signature: Option, +} + +impl ser::Writeable for StoredProofInfo { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for StoredProofInfo { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + /// Map of named accounts to BIP32 paths #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AcctPathMapping { diff --git a/tests/data/v2_reqs/init_send_tx.req.json b/tests/data/v2_reqs/init_send_tx.req.json index 1ceb75f8..8e009602 100644 --- a/tests/data/v2_reqs/init_send_tx.req.json +++ b/tests/data/v2_reqs/init_send_tx.req.json @@ -11,6 +11,7 @@ "selection_strategy_is_use_all": true, "message": "my message", "target_slate_version": null, + "payment_proof_recipient_address": null, "send_args": null } }, diff --git a/tests/data/v3_reqs/init_send_tx.req.json b/tests/data/v3_reqs/init_send_tx.req.json index b0f6587a..2fd92dbb 100644 --- a/tests/data/v3_reqs/init_send_tx.req.json +++ b/tests/data/v3_reqs/init_send_tx.req.json @@ -12,6 +12,7 @@ "selection_strategy_is_use_all": true, "message": "my message", "target_slate_version": null, + "payment_proof_recipient_address": null, "send_args": null } },