From 194fa7275096b899d337f96f9469154dd6d3c0f7 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Tue, 18 May 2021 14:15:33 +0000 Subject: [PATCH 01/17] Updates for Multisig outputs and Cargo.lock Add changes necessary for multisig outputs --- Cargo.lock | 82 ++++++++------------------ impls/src/node_clients/http.rs | 27 +++++++-- impls/src/test_framework/testclient.rs | 27 +++++++-- libwallet/src/internal/scan.rs | 14 ++++- libwallet/src/slate.rs | 3 + libwallet/src/types.rs | 9 ++- util/Cargo.toml | 12 ++-- 7 files changed, 95 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f168e4c9d..251be5e15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,7 @@ dependencies = [ "sha2 0.8.2", "subtle 2.2.3", "x25519-dalek", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -238,7 +238,7 @@ dependencies = [ "crypto-mac 0.7.0", "pbkdf2 0.3.0", "sha2 0.8.2", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -500,7 +500,7 @@ dependencies = [ "aead", "poly1305", "stream-cipher", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -747,7 +747,7 @@ dependencies = [ "digest 0.8.1", "rand_core 0.5.1", "subtle 2.2.3", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -890,7 +890,7 @@ dependencies = [ "rand 0.7.3", "serde", "sha2 0.8.2", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -955,7 +955,7 @@ dependencies = [ "proc-macro2 1.0.19", "quote 1.0.7", "syn 1.0.38", - "synstructure 0.12.4", + "synstructure", ] [[package]] @@ -1183,7 +1183,7 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "grin_api" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "bytes 0.5.6", "easy-jsonrpc-mw", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "grin_chain" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "bit-vec", "bitflags 1.2.1", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "grin_core" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "blake2-rfc", "byteorder", @@ -1261,13 +1261,13 @@ dependencies = [ "serde", "serde_derive", "siphasher", - "zeroize 1.1.0", + "zeroize", ] [[package]] name = "grin_keychain" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "blake2-rfc", "byteorder", @@ -1283,13 +1283,13 @@ dependencies = [ "serde_derive", "serde_json", "sha2 0.7.1", - "zeroize 1.1.0", + "zeroize", ] [[package]] name = "grin_p2p" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "bitflags 1.2.1", "bytes 0.5.6", @@ -1311,7 +1311,7 @@ dependencies = [ [[package]] name = "grin_pool" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "blake2-rfc", "chrono", @@ -1328,9 +1328,8 @@ dependencies = [ [[package]] name = "grin_secp256k1zkp" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca5852d12dbd4df8b7b760eaad791d191ea546bf80c46b033bbcc650b35a5c" +version = "0.7.11" +source = "git+https://github.com/geneferneau/rust-secp256k1-zkp?branch=atomic#45c27af7ac91d8c4323a19730760922348ee9a08" dependencies = [ "arrayvec 0.3.25", "cc", @@ -1339,13 +1338,13 @@ dependencies = [ "rustc-serialize", "serde", "serde_json", - "zeroize 0.9.3", + "zeroize", ] [[package]] name = "grin_store" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "byteorder", "croaring", @@ -1365,7 +1364,7 @@ dependencies = [ [[package]] name = "grin_util" version = "5.2.0-alpha.1" -source = "git+https://github.com/mimblewimble/grin?branch=master#f51b6e13761ac4c3c8e57904618ef431c14c6227" +source = "git+https://github.com/geneferneau/grin?branch=atomic#b6b9a037513d331af62bc25e86a72ebf0b6f9023" dependencies = [ "backtrace", "base64 0.12.3", @@ -1379,7 +1378,7 @@ dependencies = [ "serde", "serde_derive", "walkdir", - "zeroize 1.1.0", + "zeroize", "zip", ] @@ -3133,7 +3132,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9182278ed645df3477a9c27bfee0621c621aa16f6972635f7f795dae3d81070f" dependencies = [ - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -3432,18 +3431,6 @@ dependencies = [ "unicode-xid 0.2.1", ] -[[package]] -name = "synstructure" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "unicode-xid 0.1.0", -] - [[package]] name = "synstructure" version = "0.12.4" @@ -4100,7 +4087,7 @@ checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", - "zeroize 1.1.0", + "zeroize", ] [[package]] @@ -4118,34 +4105,13 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "zeroize" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86" -dependencies = [ - "zeroize_derive 0.9.3", -] - [[package]] name = "zeroize" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" dependencies = [ - "zeroize_derive 1.0.0", -] - -[[package]] -name = "zeroize_derive" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080616bd0e31f36095288bb0acdf1f78ef02c2fa15527d7e993f2a6c7591643e" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "synstructure 0.10.2", + "zeroize_derive", ] [[package]] @@ -4157,7 +4123,7 @@ dependencies = [ "proc-macro2 1.0.19", "quote 1.0.7", "syn 1.0.38", - "synstructure 0.12.4", + "synstructure", ] [[package]] diff --git a/impls/src/node_clients/http.rs b/impls/src/node_clients/http.rs index 6e383e811..606ffcca1 100644 --- a/impls/src/node_clients/http.rs +++ b/impls/src/node_clients/http.rs @@ -303,20 +303,34 @@ impl NodeClient for HTTPNodeClient { ( u64, u64, - Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )>, ), libwallet::Error, > { - let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = - Vec::new(); + let mut api_outputs: Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )> = Vec::new(); let params = json!([start_index, end_index, max_outputs, Some(true)]); let res = self.send_json_request::("get_unspent_outputs", ¶ms)?; // We asked for unspent outputs via the api but defensively filter out spent outputs just in case. for out in res.outputs.into_iter().filter(|out| out.spent == false) { - let is_coinbase = match out.output_type { - api::OutputType::Coinbase => true, - api::OutputType::Transaction => false, + let (is_coinbase, is_multisig) = match out.output_type { + api::OutputType::Coinbase => (true, false), + api::OutputType::Transaction => (false, false), + api::OutputType::Multisig => (false, true), }; let range_proof = match out.range_proof() { Ok(r) => r, @@ -344,6 +358,7 @@ impl NodeClient for HTTPNodeClient { out.commit, range_proof, is_coinbase, + is_multisig, block_height, out.mmr_index, )); diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index 29abfcccb..bbf53ad17 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -562,7 +562,14 @@ impl NodeClient for LocalWalletClient { ( u64, u64, - Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )>, ), libwallet::Error, > { @@ -589,18 +596,26 @@ impl NodeClient for LocalWalletClient { let m = r.recv().unwrap(); let o: api::OutputListing = serde_json::from_str(&m.body).unwrap(); - let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = - Vec::new(); + let mut api_outputs: Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )> = Vec::new(); for out in o.outputs { - let is_coinbase = match out.output_type { - api::OutputType::Coinbase => true, - api::OutputType::Transaction => false, + let (is_coinbase, is_multisig) = match out.output_type { + api::OutputType::Coinbase => (true, false), + api::OutputType::Transaction => (false, false), + api::OutputType::Multisig => (false, true), }; api_outputs.push(( out.commit, out.range_proof().unwrap(), is_coinbase, + is_multisig, out.block_height.unwrap(), out.mmr_index, )); diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs index d8cb34968..25c254355 100644 --- a/libwallet/src/internal/scan.rs +++ b/libwallet/src/internal/scan.rs @@ -49,6 +49,8 @@ struct OutputResult { pub lock_height: u64, /// pub is_coinbase: bool, + /// + pub is_multisig: bool, } #[derive(Debug, Clone)] @@ -65,7 +67,14 @@ struct RestoredTxStats { fn identify_utxo_outputs<'a, K>( keychain: &K, - outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + outputs: Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )>, status_send_channel: &Option>, percentage_complete: u8, ) -> Result, Error> @@ -79,7 +88,7 @@ where let legacy_version = HeaderVersion(1); for output in outputs.iter() { - let (commit, proof, is_coinbase, height, mmr_index) = output; + let (commit, proof, is_coinbase, is_multisig, height, mmr_index) = output; // attempt to unwind message from the RP and get a value // will fail if it's not ours let info = { @@ -136,6 +145,7 @@ where height: *height, lock_height: lock_height, is_coinbase: *is_coinbase, + is_multisig: *is_multisig, mmr_index: *mmr_index, }); } diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index f7dec6391..57c2e1590 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -400,6 +400,7 @@ impl Slate { keychain.secp(), sec_key, sec_nonce, + None, &self.pub_nonce_sum(keychain.secp())?, Some(&self.pub_blind_sum(keychain.secp())?), &self.msg_to_sign()?, @@ -580,6 +581,7 @@ impl Slate { secp, p.part_sig.as_ref().unwrap(), &self.pub_nonce_sum(secp)?, + None, &p.public_blind_excess, Some(&self.pub_blind_sum(secp)?), &self.msg_to_sign()?, @@ -895,6 +897,7 @@ impl From for OutputFeaturesV4 { let index = match of { OutputFeatures::Plain => 0, OutputFeatures::Coinbase => 1, + OutputFeatures::Multisig => 2, }; OutputFeaturesV4(index) } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 6f1ee585b..7d0ebf07a 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -362,7 +362,14 @@ pub trait NodeClient: Send + Sync + Clone { ( u64, u64, - Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + Vec<( + pedersen::Commitment, + pedersen::RangeProof, + bool, + bool, + u64, + u64, + )>, ), Error, >; diff --git a/util/Cargo.toml b/util/Cargo.toml index db1e9e40a..e8f524e84 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -35,12 +35,12 @@ sha3 = "0.8" # grin_store = { git = "https://github.com/mimblewimble/grin", tag = "v5.0.0-beta.2" } # For bleeding edge -grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } -grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } -grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" } -grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } -grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } -grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_core = { git = "https://github.com/geneferneau/grin", branch = "atomic" } +grin_keychain = { git = "https://github.com/geneferneau/grin", branch = "atomic" } +grin_chain = { git = "https://github.com/geneferneau/grin", branch = "atomic" } +grin_util = { git = "https://github.com/geneferneau/grin", branch = "atomic" } +grin_api = { git = "https://github.com/geneferneau/grin", branch = "atomic" } +grin_store = { git = "https://github.com/geneferneau/grin", branch = "atomic" } # For local testing # grin_core = { path = "../../grin/core"} From 0cc3cb102c7af25611079434da49734232e40399 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Fri, 4 Jun 2021 04:00:57 +0000 Subject: [PATCH 02/17] Add V5 Slate V5 Slate version with added features: Participant Data: * tau_x: Final step secret key for shared output rangeproofs * tau_one: Step one partial public key for shared output rangeproofs * tau_two: Step two partial public key for shared output rangeproofs --- api/src/foreign_rpc.rs | 6 +- api/src/owner_rpc.rs | 28 +- api/tests/slate_versioning.rs | 47 +- api/tests/slates/v4_req.slate | 27 ++ api/tests/slates/v5_req.slate | 27 ++ impls/src/adapters/file.rs | 4 +- impls/src/adapters/http.rs | 5 + libwallet/src/slate.rs | 486 ++++++++++++++++++- libwallet/src/slate_versions/mod.rs | 23 +- libwallet/src/slate_versions/ser.rs | 87 ++++ libwallet/src/slate_versions/v5.rs | 376 +++++++++++++++ libwallet/src/slate_versions/v5_bin.rs | 637 +++++++++++++++++++++++++ libwallet/src/slatepack/packer.rs | 6 +- libwallet/src/slatepack/types.rs | 32 +- tests/owner_v3_lifecycle.rs | 3 +- 15 files changed, 1720 insertions(+), 74 deletions(-) create mode 100644 api/tests/slates/v4_req.slate create mode 100644 api/tests/slates/v5_req.slate create mode 100644 libwallet/src/slate_versions/v5.rs create mode 100644 libwallet/src/slate_versions/v5_bin.rs diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index af7db48c9..2f285c88d 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -308,7 +308,6 @@ where dest_acct_name: Option, dest: Option, ) -> Result { - let version = in_slate.version(); let slate_from = Slate::from(in_slate); let out_slate = Foreign::receive_tx( self, @@ -317,14 +316,13 @@ where dest, ) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) } fn finalize_tx(&self, in_slate: VersionedSlate) -> Result { - let version = in_slate.version(); let out_slate = Foreign::finalize_tx(self, &Slate::from(in_slate), true).map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) } } diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 6e61195e3..1b0f94939 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -20,8 +20,8 @@ use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::{ AcctPathMapping, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack, SlatepackAddress, - StatusMessage, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, + OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, StatusMessage, + TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::{PublicKey, SecretKey}; @@ -1796,8 +1796,7 @@ where fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result { let slate = Owner::init_send_tx(self, (&token.keychain_mask).as_ref(), args) .map_err(|e| e.kind())?; - let version = SlateVersion::V4; - Ok(VersionedSlate::into_version(slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) } fn issue_invoice_tx( @@ -1807,8 +1806,7 @@ where ) -> Result { let slate = Owner::issue_invoice_tx(self, (&token.keychain_mask).as_ref(), args) .map_err(|e| e.kind())?; - let version = SlateVersion::V4; - Ok(VersionedSlate::into_version(slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) } fn process_invoice_tx( @@ -1824,8 +1822,7 @@ where args, ) .map_err(|e| e.kind())?; - let version = SlateVersion::V4; - Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) } fn finalize_tx( @@ -1839,8 +1836,7 @@ where &Slate::from(in_slate), ) .map_err(|e| e.kind())?; - let version = SlateVersion::V4; - Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate).map_err(|e| e.kind())?) } fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), ErrorKind> { @@ -1876,12 +1872,9 @@ where ) .map_err(|e| e.kind())?; match out_slate { - Some(s) => { - let version = SlateVersion::V4; - Ok(Some( - VersionedSlate::into_version(s, version).map_err(|e| e.kind())?, - )) - } + Some(s) => Ok(Some( + VersionedSlate::into_version(s, s.version()).map_err(|e| e.kind())?, + )), None => Ok(None), } } @@ -2082,8 +2075,7 @@ where secret_indices, ) .map_err(|e| e.kind())?; - let version = SlateVersion::V4; - Ok(VersionedSlate::into_version(slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) } fn decode_slatepack_message( diff --git a/api/tests/slate_versioning.rs b/api/tests/slate_versioning.rs index ede8453b7..54cbf7392 100644 --- a/api/tests/slate_versioning.rs +++ b/api/tests/slate_versioning.rs @@ -12,9 +12,9 @@ // limitations under the License. //! core::libtx specific tests -//use grin_wallet_api::foreign_rpc_client; +use grin_wallet_api::foreign_rpc_client; use grin_wallet_api::run_doctest_foreign; -//use grin_wallet_libwallet::VersionedSlate; +use grin_wallet_libwallet::{Slate, SlateVersion, VersionedSlate}; use serde_json; use serde_json::Value; use tempfile::tempdir; @@ -83,52 +83,45 @@ fn receive_tx(vs: VersionedSlate) -> VersionedSlate { ) .unwrap(); let (call, tracker) = bound_method.call(); - let json_response = run_doctest_foreign(call.as_request(), dir, 5, false, false) + let json_response = run_doctest_foreign(call.as_request(), dir, true, 5, false, false) .unwrap() .unwrap(); - let mut response = easy_jsonrpc::Response::from_json_response(json_response).unwrap(); + let mut response = easy_jsonrpc_mw::Response::from_json_response(json_response).unwrap(); tracker.get_return(&mut response).unwrap().unwrap() } #[test] fn version_unchanged() { - let req: Value = serde_json::from_str(include_str!("slates/v1_req.slate")).unwrap(); - let slate: VersionedSlate = serde_json::from_value(req["params"][0].clone()).unwrap(); - let slate_req: Slate = slate.into(); + let req_v4: Value = serde_json::from_str(include_str!("slates/v4_req.slate")).unwrap(); + let slate: VersionedSlate = serde_json::from_value(req_v4["params"][0].clone()).unwrap(); + let mut slate_req: Slate = slate.into(); assert_eq!( receive_tx(VersionedSlate::into_version( - slate_req.clone(), - SlateVersion::V0 - )) + slate_req.clone() + ).unwrap()) .version(), - SlateVersion::V0 + SlateVersion::V4 ); - assert_eq!( - receive_tx(VersionedSlate::into_version( - slate_req.clone(), - SlateVersion::V1 - )) - .version(), - SlateVersion::V1 - ); + let req_v5: Value = serde_json::from_str(include_str!("slates/v5_req.slate")).unwrap(); + let slate: VersionedSlate = serde_json::from_value(req_v5["params"][0].clone()).unwrap(); + slate_req = slate.into(); assert_eq!( receive_tx(VersionedSlate::into_version( - slate_req.clone(), - SlateVersion::V2 - )) + slate_req.clone() + ).unwrap()) .version(), - SlateVersion::V2 + SlateVersion::V5 ); // compile time test will remind us to update these tests when updating slate format fn _all_versions_tested(vs: VersionedSlate) { match vs { - VersionedSlate::V0(_) => (), - VersionedSlate::V1(_) => (), - VersionedSlate::V2(_) => (), + VersionedSlate::V4(_) => (), + VersionedSlate::V5(_) => (), } } -}*/ +} +*/ diff --git a/api/tests/slates/v4_req.slate b/api/tests/slates/v4_req.slate new file mode 100644 index 000000000..b4b63cf07 --- /dev/null +++ b/api/tests/slates/v4_req.slate @@ -0,0 +1,27 @@ +{ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + { + "amt": "6000000000", + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + } + ], + "sta": "S1", + "ver": "4:2" + }, + null, + null + ] +} diff --git a/api/tests/slates/v5_req.slate b/api/tests/slates/v5_req.slate new file mode 100644 index 000000000..bf39108db --- /dev/null +++ b/api/tests/slates/v5_req.slate @@ -0,0 +1,27 @@ +{ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + { + "amt": "6000000000", + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + } + ], + "sta": "S1", + "ver": "5:2" + }, + null, + null + ] +} diff --git a/impls/src/adapters/file.rs b/impls/src/adapters/file.rs index b65803edb..134b9b2a3 100644 --- a/impls/src/adapters/file.rs +++ b/impls/src/adapters/file.rs @@ -16,7 +16,7 @@ use std::fs::File; use std::io::{Read, Write}; -use crate::libwallet::{Error, ErrorKind, Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; +use crate::libwallet::{Error, ErrorKind, Slate, VersionedBinSlate, VersionedSlate}; use crate::{SlateGetter, SlatePutter}; use grin_wallet_util::byte_ser; use std::convert::TryFrom; @@ -37,7 +37,7 @@ impl SlatePutter for PathToSlate { }*/ let mut pub_tx = File::create(&self.0)?; // TODO: - let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + let out_slate = VersionedSlate::into_version(slate.clone(), slate.version())?; if as_bin { let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| ErrorKind::SlateSer)?; diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index 7d9f360d7..7155e8cca 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -145,6 +145,10 @@ impl HttpSlateSender { return Err(ErrorKind::ClientCallback(report).into()); } + if supported_slate_versions.contains(&"V5".to_owned()) { + return Ok(SlateVersion::V5); + } + if supported_slate_versions.contains(&"V4".to_owned()) { return Ok(SlateVersion::V4); } @@ -190,6 +194,7 @@ impl SlateSender for HttpSlateSender { let slate_send = match self.check_other_version(&url_str)? { SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?, + SlateVersion::V5 => VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?, }; // Note: not using easy-jsonrpc as don't want the dependencies in this crate let req = match finalize { diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 57c2e1590..061225e2e 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -39,7 +39,11 @@ use crate::slate_versions::v4::{ CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4, SlateStateV4, SlateV4, VersionCompatInfoV4, }; -use crate::slate_versions::VersionedSlate; +use crate::slate_versions::v5::{ + CommitsV5, KernelFeaturesArgsV5, OutputFeaturesV5, ParticipantDataV5, PaymentInfoV5, + SlateStateV5, SlateV5, VersionCompatInfoV5, +}; +use crate::slate_versions::{SlateVersion, VersionedSlate}; use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; use crate::Context; @@ -62,6 +66,12 @@ pub struct ParticipantData { pub public_nonce: PublicKey, /// Public partial signature pub part_sig: Option, + /// Tau X key for multiparty output rangeproof + pub tau_x: Option, + /// Tau one partial public key for multiparty output rangeproof + pub tau_one: Option, + /// Tau two partial public key for multiparty output rangeproof + pub tau_two: Option, } impl ParticipantData { @@ -98,7 +108,7 @@ pub struct Slate { pub state: SlateState, /// The core transaction data: /// inputs, outputs, kernels, kernel offset - /// Optional as of V4 to allow for a compact + /// Optional as of V5 to allow for a compact /// transaction initiation pub tx: Option, /// base amount (excluding fee) @@ -226,10 +236,10 @@ impl Slate { /// Upgrade a versioned slate pub fn upgrade(v_slate: VersionedSlate) -> Result { - let v4: SlateV4 = match v_slate { - VersionedSlate::V4(s) => s, - }; - Ok(v4.into()) + match v_slate { + VersionedSlate::V4(s) => Ok(s.into()), + VersionedSlate::V5(s) => Ok(s.into()), + } } /// Compact the slate for initial sending, storing the excess + offset explicit /// and removing my input/output data @@ -510,6 +520,9 @@ impl Slate { public_blind_excess: pub_key, public_nonce: pub_nonce, part_sig: part_sig, + tau_x: None, + tau_one: None, + tau_two: None, }); Ok(()) } @@ -683,6 +696,14 @@ impl Slate { Ok(()) } } + + /// Get the SlateVersion of the Slate + pub fn version(&self) -> SlateVersion { + match self.version_info.version { + 4 => SlateVersion::V4, + 5 | _ => SlateVersion::V5, + } + } } impl Serialize for Slate { @@ -690,8 +711,16 @@ impl Serialize for Slate { where S: Serializer, { - let v4 = SlateV4::from(self); - v4.serialize(serializer) + match self.version() { + SlateVersion::V4 => { + let v4 = SlateV4::from(self); + v4.serialize(serializer) + } + SlateVersion::V5 => { + let v5 = SlateV5::from(self); + v5.serialize(serializer) + } + } } } // Current slate version to versioned conversions @@ -743,6 +772,53 @@ impl From for SlateV4 { } } +// Slate to versioned +impl From for SlateV5 { + fn from(slate: Slate) -> SlateV5 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset: off, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(&version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(&p)), + None => None, + }; + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(&a)), + None => None, + }; + let sta = SlateStateV5::from(&state); + SlateV5 { + num_parts, + id, + sta, + coms: (&slate).into(), + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + impl From<&Slate> for SlateV4 { fn from(slate: &Slate) -> SlateV4 { let Slate { @@ -796,6 +872,59 @@ impl From<&Slate> for SlateV4 { } } +impl From<&Slate> for SlateV5 { + fn from(slate: &Slate) -> SlateV5 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate; + let num_parts = *num_parts; + let id = *id; + let amount = *amount; + let fee_fields = *fee_fields; + let feat = *kernel_features; + let ttl = *ttl; + let off = offset.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(p)), + None => None, + }; + let sta = SlateStateV5::from(state); + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(a)), + None => None, + }; + SlateV5 { + num_parts, + id, + sta, + coms: slate.into(), + amt: amount, + fee: fee_fields, + feat, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + impl From<&Slate> for Option> { fn from(slate: &Slate) -> Self { match slate.tx { @@ -819,12 +948,38 @@ impl From<&Slate> for Option> { } } +impl From<&Slate> for Option> { + fn from(slate: &Slate) -> Self { + match slate.tx { + None => None, + Some(ref tx) => { + let mut ret_vec = vec![]; + match tx.inputs() { + Inputs::CommitOnly(_) => panic!("commit only inputs unsupported"), + Inputs::FeaturesAndCommit(ref inputs) => { + for input in inputs { + ret_vec.push(input.into()); + } + } + } + for output in tx.outputs() { + ret_vec.push(output.into()); + } + Some(ret_vec) + } + } + } +} + impl From<&ParticipantData> for ParticipantDataV4 { fn from(data: &ParticipantData) -> ParticipantDataV4 { let ParticipantData { public_blind_excess, public_nonce, part_sig, + tau_x: _, + tau_one: _, + tau_two: _, } = data; let public_blind_excess = *public_blind_excess; let public_nonce = *public_nonce; @@ -837,6 +992,33 @@ impl From<&ParticipantData> for ParticipantDataV4 { } } +impl From<&ParticipantData> for ParticipantDataV5 { + fn from(data: &ParticipantData) -> ParticipantDataV5 { + let ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + tau_x, + tau_one, + tau_two, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + let tau_x = tau_x.clone(); + let tau_one = tau_one.clone(); + let tau_two = tau_two.clone(); + ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + tau_x, + tau_one, + tau_two, + } + } +} + impl From<&SlateState> for SlateStateV4 { fn from(data: &SlateState) -> SlateStateV4 { match data { @@ -851,6 +1033,20 @@ impl From<&SlateState> for SlateStateV4 { } } +impl From<&SlateState> for SlateStateV5 { + fn from(data: &SlateState) -> SlateStateV5 { + match data { + SlateState::Unknown => SlateStateV5::Unknown, + SlateState::Standard1 => SlateStateV5::Standard1, + SlateState::Standard2 => SlateStateV5::Standard2, + SlateState::Standard3 => SlateStateV5::Standard3, + SlateState::Invoice1 => SlateStateV5::Invoice1, + SlateState::Invoice2 => SlateStateV5::Invoice2, + SlateState::Invoice3 => SlateStateV5::Invoice3, + } + } +} + impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV4 { fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV4 { let KernelFeaturesArgs { lock_height } = data; @@ -859,6 +1055,14 @@ impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV4 { } } +impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV5 { + fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV5 { + let KernelFeaturesArgs { lock_height } = data; + let lock_hgt = *lock_height; + KernelFeaturesArgsV5 { lock_hgt } + } +} + impl From<&VersionCompatInfo> for VersionCompatInfoV4 { fn from(data: &VersionCompatInfo) -> VersionCompatInfoV4 { let VersionCompatInfo { @@ -874,6 +1078,21 @@ impl From<&VersionCompatInfo> for VersionCompatInfoV4 { } } +impl From<&VersionCompatInfo> for VersionCompatInfoV5 { + fn from(data: &VersionCompatInfo) -> VersionCompatInfoV5 { + let VersionCompatInfo { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfoV5 { + version, + block_header_version, + } + } +} + impl From<&PaymentInfo> for PaymentInfoV4 { fn from(data: &PaymentInfo) -> PaymentInfoV4 { let PaymentInfo { @@ -892,6 +1111,24 @@ impl From<&PaymentInfo> for PaymentInfoV4 { } } +impl From<&PaymentInfo> for PaymentInfoV5 { + fn from(data: &PaymentInfo) -> PaymentInfoV5 { + let PaymentInfo { + sender_address, + receiver_address, + receiver_signature, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let receiver_signature = *receiver_signature; + PaymentInfoV5 { + saddr: sender_address, + raddr: receiver_address, + rsig: receiver_signature, + } + } +} + impl From for OutputFeaturesV4 { fn from(of: OutputFeatures) -> OutputFeaturesV4 { let index = match of { @@ -903,6 +1140,17 @@ impl From for OutputFeaturesV4 { } } +impl From for OutputFeaturesV5 { + fn from(of: OutputFeatures) -> OutputFeaturesV5 { + let index = match of { + OutputFeatures::Plain => 0, + OutputFeatures::Coinbase => 1, + OutputFeatures::Multisig => 2, + }; + OutputFeaturesV5(index) + } +} + // Versioned to current slate impl From for Slate { fn from(slate: SlateV4) -> Slate { @@ -950,6 +1198,53 @@ impl From for Slate { } } +// Versioned to current slate +impl From for Slate { + fn from(slate: SlateV5) -> Slate { + let SlateV5 { + num_parts: num_participants, + id, + sta, + coms: _, + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl: ttl_cutoff_height, + off: offset, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); + let version_info = VersionCompatInfo::from(&ver); + let payment_proof = match &payment_proof { + Some(p) => Some(PaymentInfo::from(p)), + None => None, + }; + let kernel_features_args = match &feat_args { + Some(a) => Some(KernelFeaturesArgs::from(a)), + None => None, + }; + let state = SlateState::from(&sta); + Slate { + num_participants, + id, + state, + tx: (&slate).into(), + amount, + fee_fields, + kernel_features, + ttl_cutoff_height, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } + } +} + pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { let coms = match slate.coms.as_ref() { Some(c) => c, @@ -964,6 +1259,9 @@ pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { public_blind_excess: d.xs, public_nonce: d.nonce, part_sig: d.part, + tau_x: None, + tau_one: None, + tau_two: None, }); } let excess = match calc_slate.calc_excess(&secp) { @@ -1016,13 +1314,89 @@ pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { Some(tx) } -// Node's Transaction object and lock height to SlateV4 `coms` +pub fn tx_from_slate_v5(slate: &SlateV5) -> Option { + let coms = match slate.coms.as_ref() { + Some(c) => c, + None => return None, + }; + let secp = static_secp_instance(); + let secp = secp.lock(); + let mut calc_slate = Slate::blank(2, false); + calc_slate.fee_fields = slate.fee; + for d in slate.sigs.iter() { + calc_slate.participant_data.push(ParticipantData { + public_blind_excess: d.xs, + public_nonce: d.nonce, + part_sig: d.part, + tau_x: d.tau_x.clone(), + tau_one: d.tau_one.clone(), + tau_two: d.tau_two.clone(), + }); + } + let excess = match calc_slate.calc_excess(&secp) { + Ok(e) => e, + Err(_) => Commitment::from_vec(vec![0]), + }; + let excess_sig = match calc_slate.finalize_signature(&secp) { + Ok(s) => s, + Err(_) => Signature::from_raw_data(&[0; 64]).unwrap(), + }; + let kernel = TxKernel { + features: match slate.feat { + 0 => KernelFeatures::Plain { fee: slate.fee }, + 1 => KernelFeatures::HeightLocked { + fee: slate.fee, + lock_height: match slate.feat_args.as_ref() { + Some(a) => a.lock_hgt, + None => 0, + }, + }, + _ => KernelFeatures::Plain { fee: slate.fee }, + }, + excess, + excess_sig, + }; + let mut tx = Slate::empty_transaction().with_kernel(kernel); + + let mut outputs = vec![]; + let mut inputs = vec![]; + + for c in coms.iter() { + match &c.p { + Some(p) => { + outputs.push(Output::new(c.f.into(), c.c, p.clone())); + } + None => { + inputs.push(Input { + features: c.f.into(), + commit: c.c, + }); + } + } + } + + tx.body = tx + .body + .replace_inputs(inputs.as_slice().into()) + .replace_outputs(outputs.as_slice()); + tx.offset = slate.off.clone(); + Some(tx) +} + +// Node's Transaction object and lock height to SlateV5 `coms` impl From<&SlateV4> for Option { fn from(slate: &SlateV4) -> Option { tx_from_slate_v4(slate) } } +// Node's Transaction object and lock height to SlateV5 `coms` +impl From<&SlateV5> for Option { + fn from(slate: &SlateV5) -> Option { + tx_from_slate_v5(slate) + } +} + impl From<&ParticipantDataV4> for ParticipantData { fn from(data: &ParticipantDataV4) -> ParticipantData { let ParticipantDataV4 { @@ -1037,6 +1411,36 @@ impl From<&ParticipantDataV4> for ParticipantData { public_blind_excess, public_nonce, part_sig, + tau_x: None, + tau_one: None, + tau_two: None, + } + } +} + +impl From<&ParticipantDataV5> for ParticipantData { + fn from(data: &ParticipantDataV5) -> ParticipantData { + let ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + tau_x, + tau_one, + tau_two, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + let tau_x = tau_x.clone(); + let tau_one = tau_one.clone(); + let tau_two = tau_two.clone(); + ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + tau_x, + tau_one, + tau_two, } } } @@ -1049,6 +1453,14 @@ impl From<&KernelFeaturesArgsV4> for KernelFeaturesArgs { } } +impl From<&KernelFeaturesArgsV5> for KernelFeaturesArgs { + fn from(data: &KernelFeaturesArgsV5) -> KernelFeaturesArgs { + let KernelFeaturesArgsV5 { lock_hgt } = data; + let lock_height = *lock_hgt; + KernelFeaturesArgs { lock_height } + } +} + impl From<&SlateStateV4> for SlateState { fn from(data: &SlateStateV4) -> SlateState { match data { @@ -1063,6 +1475,20 @@ impl From<&SlateStateV4> for SlateState { } } +impl From<&SlateStateV5> for SlateState { + fn from(data: &SlateStateV5) -> SlateState { + match data { + SlateStateV5::Unknown => SlateState::Unknown, + SlateStateV5::Standard1 => SlateState::Standard1, + SlateStateV5::Standard2 => SlateState::Standard2, + SlateStateV5::Standard3 => SlateState::Standard3, + SlateStateV5::Invoice1 => SlateState::Invoice1, + SlateStateV5::Invoice2 => SlateState::Invoice2, + SlateStateV5::Invoice3 => SlateState::Invoice3, + } + } +} + impl From<&VersionCompatInfoV4> for VersionCompatInfo { fn from(data: &VersionCompatInfoV4) -> VersionCompatInfo { let VersionCompatInfoV4 { @@ -1078,6 +1504,21 @@ impl From<&VersionCompatInfoV4> for VersionCompatInfo { } } +impl From<&VersionCompatInfoV5> for VersionCompatInfo { + fn from(data: &VersionCompatInfoV5) -> VersionCompatInfo { + let VersionCompatInfoV5 { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfo { + version, + block_header_version, + } + } +} + impl From<&PaymentInfoV4> for PaymentInfo { fn from(data: &PaymentInfoV4) -> PaymentInfo { let PaymentInfoV4 { @@ -1096,6 +1537,24 @@ impl From<&PaymentInfoV4> for PaymentInfo { } } +impl From<&PaymentInfoV5> for PaymentInfo { + fn from(data: &PaymentInfoV5) -> PaymentInfo { + let PaymentInfoV5 { + saddr: sender_address, + raddr: receiver_address, + rsig: receiver_signature, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let receiver_signature = *receiver_signature; + PaymentInfo { + sender_address, + receiver_address, + receiver_signature, + } + } +} + impl From for OutputFeatures { fn from(of: OutputFeaturesV4) -> OutputFeatures { match of.0 { @@ -1104,3 +1563,12 @@ impl From for OutputFeatures { } } } + +impl From for OutputFeatures { + fn from(of: OutputFeaturesV5) -> OutputFeatures { + match of.0 { + 1 => OutputFeatures::Coinbase, + 0 | _ => OutputFeatures::Plain, + } + } +} diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index ca2e749f8..4f63d5ca1 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -20,6 +20,8 @@ use crate::slate::Slate; use crate::slate_versions::v4::{CoinbaseV4, SlateV4}; use crate::slate_versions::v4_bin::SlateV4Bin; +use crate::slate_versions::v5::{CoinbaseV5, SlateV5}; +use crate::slate_versions::v5_bin::SlateV5Bin; use crate::types::CbData; use crate::Error; use std::convert::TryFrom; @@ -31,8 +33,13 @@ pub mod v4; #[allow(missing_docs)] pub mod v4_bin; +#[allow(missing_docs)] +pub mod v5; +#[allow(missing_docs)] +pub mod v5_bin; + /// The most recent version of the slate -pub const CURRENT_SLATE_VERSION: u16 = 4; +pub const CURRENT_SLATE_VERSION: u16 = 5; /// The grin block header this slate is intended to be compatible with pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; @@ -40,6 +47,8 @@ pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; /// Existing versions of the slate #[derive(EnumIter, Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] pub enum SlateVersion { + /// V5 (next version) + V5, /// V4 (most current) V4, } @@ -49,6 +58,8 @@ pub enum SlateVersion { /// Versions are ordered newest to oldest so serde attempts to /// deserialize newer versions first, then falls back to older versions. pub enum VersionedSlate { + /// Next (5.1.0 Onwards) + V5(SlateV5), /// Current (4.0.0 Onwards ) V4(SlateV4), } @@ -58,6 +69,7 @@ impl VersionedSlate { pub fn version(&self) -> SlateVersion { match *self { VersionedSlate::V4(_) => SlateVersion::V4, + VersionedSlate::V5(_) => SlateVersion::V5, } } @@ -65,6 +77,7 @@ impl VersionedSlate { pub fn into_version(slate: Slate, version: SlateVersion) -> Result { match version { SlateVersion::V4 => Ok(VersionedSlate::V4(slate.into())), + SlateVersion::V5 => Ok(VersionedSlate::V5(slate.into())), } } } @@ -73,6 +86,7 @@ impl From for Slate { fn from(slate: VersionedSlate) -> Slate { match slate { VersionedSlate::V4(s) => Slate::from(s), + VersionedSlate::V5(s) => Slate::from(s), } } } @@ -82,6 +96,8 @@ impl From for Slate { /// Binary versions, can only be parsed 1:1 into the appropriate /// version, and VersionedSlate can up/downgrade from there pub enum VersionedBinSlate { + /// Version 5, binary + V5(SlateV5Bin), /// Version 4, binary V4(SlateV4Bin), } @@ -91,6 +107,7 @@ impl TryFrom for VersionedBinSlate { fn try_from(slate: VersionedSlate) -> Result { match slate { VersionedSlate::V4(s) => Ok(VersionedBinSlate::V4(SlateV4Bin(s))), + VersionedSlate::V5(s) => Ok(VersionedBinSlate::V5(SlateV5Bin(s))), } } } @@ -99,6 +116,7 @@ impl From for VersionedSlate { fn from(slate: VersionedBinSlate) -> VersionedSlate { match slate { VersionedBinSlate::V4(s) => VersionedSlate::V4(s.0), + VersionedBinSlate::V5(s) => VersionedSlate::V5(s.0), } } } @@ -108,6 +126,8 @@ impl From for VersionedSlate { /// Versions are ordered newest to oldest so serde attempts to /// deserialize newer versions first, then falls back to older versions. pub enum VersionedCoinbase { + /// Next supported coinbase version. + V5(CoinbaseV5), /// Current supported coinbase version. V4(CoinbaseV4), } @@ -117,6 +137,7 @@ impl VersionedCoinbase { pub fn into_version(cb: CbData, version: SlateVersion) -> VersionedCoinbase { match version { SlateVersion::V4 => VersionedCoinbase::V4(cb.into()), + SlateVersion::V5 => VersionedCoinbase::V5(cb.into()), } } } diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 58f866a08..7e28da0f0 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -514,6 +514,48 @@ pub mod version_info_v4 { } } +/// Serializes slates 'version_info' field +pub mod version_info_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::VersionCompatInfoV5; + + /// + pub fn serialize(v: &VersionCompatInfoV5, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}:{}", v.version, v.block_header_version)) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let mut retval = VersionCompatInfoV5 { + version: 0, + block_header_version: 0, + }; + let v: Vec<&str> = s.split(':').collect(); + if v.len() != 2 { + return Err(Error::custom("Cannot parse version")); + } + match u16::from_str_radix(v[0], 10) { + Ok(u) => retval.version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + match u16::from_str_radix(v[1], 10) { + Ok(u) => retval.block_header_version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + Ok(retval) + }) + } +} + /// Serializes slates 'state' field pub mod slate_state_v4 { use serde::de::Error; @@ -559,6 +601,51 @@ pub mod slate_state_v4 { } } +/// Serializes slates 'state' field +pub mod slate_state_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::SlateStateV5; + + /// + pub fn serialize(st: &SlateStateV5, serializer: S) -> Result + where + S: Serializer, + { + let label = match st { + SlateStateV5::Unknown => "NA", + SlateStateV5::Standard1 => "S1", + SlateStateV5::Standard2 => "S2", + SlateStateV5::Standard3 => "S3", + SlateStateV5::Invoice1 => "I1", + SlateStateV5::Invoice2 => "I2", + SlateStateV5::Invoice3 => "I3", + }; + serializer.serialize_str(label) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let retval = match s.as_str() { + "NA" => SlateStateV5::Unknown, + "S1" => SlateStateV5::Standard1, + "S2" => SlateStateV5::Standard2, + "S3" => SlateStateV5::Standard3, + "I1" => SlateStateV5::Invoice1, + "I2" => SlateStateV5::Invoice2, + "I3" => SlateStateV5::Invoice3, + _ => return Err(Error::custom("Invalid Slate state")), + }; + Ok(retval) + }) + } +} + /// Serializes an secp256k1 pubkey to base64 pub mod uuid_base64 { use base64; diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs new file mode 100644 index 000000000..ebaf3f210 --- /dev/null +++ b/libwallet/src/slate_versions/v5.rs @@ -0,0 +1,376 @@ +// Copyright 2021 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. + +//! Contains V5 of the slate (grin-wallet 5.1.0) +//! Changes from V4: +//! /#### ParticipantData Struct +//! +//! * `tau_x` is added for creating multisig output range proofs +//! * `tau_one` is added for creating multisig output range proofs +//! * `tau_two` is added for creating multisig output range proofs + +use crate::grin_core::core::FeeFields; +use crate::grin_core::core::{Input, Output, TxKernel}; +use crate::grin_core::libtx::secp_ser; +use crate::grin_keychain::{BlindingFactor, Identifier}; +use crate::grin_util::secp; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use crate::{slate_versions::ser, CbData}; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlateV5 { + // Required Fields + /// Versioning info + #[serde(with = "ser::version_info_v5")] + pub ver: VersionCompatInfoV5, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// Slate state + #[serde(with = "ser::slate_state_v5")] + pub sta: SlateStateV5, + /// Offset, modified by each participant inserting inputs + /// as the transaction progresses + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::blind_from_hex" + )] + #[serde(default = "default_offset_zero")] + #[serde(skip_serializing_if = "offset_is_zero")] + pub off: BlindingFactor, + // Optional fields depending on state + /// The number of participants intended to take part in this transaction + #[serde(default = "default_num_participants_2")] + #[serde(skip_serializing_if = "num_parts_is_2")] + pub num_parts: u8, + /// base amount (excluding fee) + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub amt: u64, + /// fee + #[serde(skip_serializing_if = "fee_is_zero")] + #[serde(default = "default_fee")] + pub fee: FeeFields, + /// kernel features, if any + #[serde(skip_serializing_if = "u8_is_blank")] + #[serde(default = "default_u8_0")] + pub feat: u8, + /// TTL, the block height at which wallets + /// should refuse to process the transaction and unlock all + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub ttl: u64, + // Structs always required + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub sigs: Vec, + // Situational, but required at some point in the tx + /// Inputs/Output commits added to slate + #[serde(default = "default_coms_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub coms: Option>, + // Optional Structs + /// Payment Proof + #[serde(default = "default_payment_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + /// Kernel features arguments + #[serde(default = "default_kernel_features_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub feat_args: Option, +} + +fn default_payment_none() -> Option { + None +} + +fn default_offset_zero() -> BlindingFactor { + BlindingFactor::zero() +} + +fn offset_is_zero(o: &BlindingFactor) -> bool { + *o == BlindingFactor::zero() +} + +fn default_coms_none() -> Option> { + None +} + +fn default_u64_0() -> u64 { + 0 +} + +fn num_parts_is_2(n: &u8) -> bool { + *n == 2 +} + +fn default_num_participants_2() -> u8 { + 2 +} + +fn default_kernel_features_none() -> Option { + None +} + +/// Slate state definition +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SlateStateV5 { + /// Unknown, coming from earlier versions of the slate + Unknown, + /// Standard flow, freshly init + Standard1, + /// Standard flow, return journey + Standard2, + /// Standard flow, ready for transaction posting + Standard3, + /// Invoice flow, freshly init + Invoice1, + ///Invoice flow, return journey + Invoice2, + /// Invoice flow, ready for tranasction posting + Invoice3, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Kernel features arguments definition +pub struct KernelFeaturesArgsV5 { + /// Lock height, for HeightLocked + pub lock_hgt: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct VersionCompatInfoV5 { + /// The current version of the slate format + pub version: u16, + /// Version of grin block header this slate is compatible with + pub block_header_version: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ParticipantDataV5 { + /// Public key corresponding to private blinding factor + #[serde(with = "secp_ser::pubkey_serde")] + pub xs: PublicKey, + /// Public key corresponding to private nonce + #[serde(with = "secp_ser::pubkey_serde")] + pub nonce: PublicKey, + /// Public partial signature + #[serde(default = "default_part_sig_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_sig_serde")] + pub part: Option, + /// Tau X key for shared outputs + #[serde(default = "default_tau_x_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub tau_x: Option, + /// Tau part one key for shared outputs + #[serde(default = "default_tau_part_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_pubkey_serde")] + pub tau_one: Option, + /// Tau part two key for shared outputs + #[serde(default = "default_tau_part_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_pubkey_serde")] + pub tau_two: Option, +} + +fn default_part_sig_none() -> Option { + None +} + +fn default_tau_x_none() -> Option { + None +} + +fn default_tau_part_none() -> Option { + None +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PaymentInfoV5 { + #[serde(with = "ser::dalek_pubkey_serde")] + pub saddr: DalekPublicKey, + #[serde(with = "ser::dalek_pubkey_serde")] + pub raddr: DalekPublicKey, + #[serde(default = "default_receiver_signature_none")] + #[serde(with = "ser::option_dalek_sig_serde")] + #[serde(skip_serializing_if = "Option::is_none")] + pub rsig: Option, +} + +fn default_receiver_signature_none() -> Option { + None +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct CommitsV5 { + /// Options for an output's structure or use + #[serde(default = "default_output_feature")] + #[serde(skip_serializing_if = "output_feature_is_plain")] + pub f: OutputFeaturesV5, + /// The homomorphic commitment representing the output amount + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub c: Commitment, + /// A proof that the commitment is in the right range + /// Only applies for transaction outputs + #[serde(with = "ser::option_rangeproof_hex")] + #[serde(default = "default_range_proof")] + #[serde(skip_serializing_if = "Option::is_none")] + pub p: Option, +} + +impl From<&Output> for CommitsV5 { + fn from(out: &Output) -> CommitsV5 { + CommitsV5 { + f: out.features().into(), + c: out.commitment(), + p: Some(out.proof()), + } + } +} + +// This will need to be reworked once we no longer support input features with "commit only" inputs. +impl From<&Input> for CommitsV5 { + fn from(input: &Input) -> CommitsV5 { + CommitsV5 { + f: input.features.into(), + c: input.commitment(), + p: None, + } + } +} + +fn default_output_feature() -> OutputFeaturesV5 { + OutputFeaturesV5(0) +} + +fn output_feature_is_plain(o: &OutputFeaturesV5) -> bool { + o.0 == 0 +} + +#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq, Eq)] +pub struct OutputFeaturesV5(pub u8); + +pub fn sig_is_blank(s: &secp::Signature) -> bool { + for b in s.to_raw_data().iter() { + if *b != 0 { + return false; + } + } + true +} + +fn default_range_proof() -> Option { + None +} + +fn u64_is_blank(u: &u64) -> bool { + *u == 0 +} + +fn default_u8_0() -> u8 { + 0 +} + +fn u8_is_blank(u: &u8) -> bool { + *u == 0 +} + +fn fee_is_zero(f: &FeeFields) -> bool { + f.is_zero() +} + +fn default_fee() -> FeeFields { + FeeFields::zero() +} + +/// A mining node requests new coinbase via the foreign api every time a new candidate block is built. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinbaseV5 { + /// Output + output: CbOutputV5, + /// Kernel + kernel: CbKernelV5, + /// Key Id + key_id: Option, +} + +impl From for CoinbaseV5 { + fn from(cb: CbData) -> CoinbaseV5 { + CoinbaseV5 { + output: CbOutputV5::from(&cb.output), + kernel: CbKernelV5::from(&cb.kernel), + key_id: cb.key_id, + } + } +} + +impl From<&Output> for CbOutputV5 { + fn from(output: &Output) -> CbOutputV5 { + CbOutputV5 { + features: CbOutputFeatures::Coinbase, + commit: output.commitment(), + proof: output.proof(), + } + } +} + +impl From<&TxKernel> for CbKernelV5 { + fn from(kernel: &TxKernel) -> CbKernelV5 { + CbKernelV5 { + features: CbKernelFeatures::Coinbase, + excess: kernel.excess, + excess_sig: kernel.excess_sig, + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbOutputFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbKernelFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +struct CbOutputV5 { + features: CbOutputFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + commit: Commitment, + #[serde(serialize_with = "secp_ser::as_hex")] + proof: RangeProof, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CbKernelV5 { + features: CbKernelFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + excess: Commitment, + #[serde(with = "secp_ser::sig_serde")] + excess_sig: secp::Signature, +} diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs new file mode 100644 index 000000000..00ba8f21a --- /dev/null +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -0,0 +1,637 @@ +// Copyright 2021 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. + +//! Wraps a V5 Slate into a V5 Binary slate + +use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +use crate::grin_keychain::BlindingFactor; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use crate::grin_util::static_secp_instance; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use std::convert::TryFrom; +use uuid::Uuid; + +use crate::slate_versions::v5::{ + CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, SlateStateV5, SlateV5, + VersionCompatInfoV5, +}; + +impl Writeable for SlateStateV5 { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let b = match self { + SlateStateV5::Unknown => 0, + SlateStateV5::Standard1 => 1, + SlateStateV5::Standard2 => 2, + SlateStateV5::Standard3 => 3, + SlateStateV5::Invoice1 => 4, + SlateStateV5::Invoice2 => 5, + SlateStateV5::Invoice3 => 6, + }; + writer.write_u8(b) + } +} + +impl Readable for SlateStateV5 { + fn read(reader: &mut R) -> Result { + let b = reader.read_u8()?; + let sta = match b { + 0 => SlateStateV5::Unknown, + 1 => SlateStateV5::Standard1, + 2 => SlateStateV5::Standard2, + 3 => SlateStateV5::Standard3, + 4 => SlateStateV5::Invoice1, + 5 => SlateStateV5::Invoice2, + 6 => SlateStateV5::Invoice3, + _ => SlateStateV5::Unknown, + }; + Ok(sta) + } +} + +/// Allow serializing of Uuids not defined in crate +struct UuidWrap(Uuid); + +impl Writeable for UuidWrap { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_fixed_bytes(&self.0.as_bytes()) + } +} + +impl Readable for UuidWrap { + fn read(reader: &mut R) -> Result { + let bytes = reader.read_fixed_bytes(16)?; + let mut b = [0u8; 16]; + b.copy_from_slice(&bytes[0..16]); + Ok(UuidWrap(Uuid::from_bytes(b))) + } +} + +/// Helper struct to serialize optional fields efficiently +struct SlateOptFields { + /// num parts, default 2 + pub num_parts: u8, + /// amt, default 0 + pub amt: u64, + /// fee_fields, default FeeFields::zero() + pub fee: FeeFields, + /// kernel features, default 0 + pub feat: u8, + /// ttl, default 0 + pub ttl: u64, +} + +impl Writeable for SlateOptFields { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional fields are serialized + // 0 0 0 1 1 1 1 1 + // t f f a n + let mut status = 0u8; + if self.num_parts != 2 { + status |= 0x01; + } + if self.amt > 0 { + status |= 0x02; + } + if self.fee.fee() > 0 { + // apply fee mask past HF4 + status |= 0x04; + } + if self.feat > 0 { + status |= 0x08; + } + if self.ttl > 0 { + status |= 0x10; + } + writer.write_u8(status)?; + if status & 0x01 > 0 { + writer.write_u8(self.num_parts)?; + } + if status & 0x02 > 0 { + writer.write_u64(self.amt)?; + } + if status & 0x04 > 0 { + self.fee.write(writer)?; + } + if status & 0x08 > 0 { + writer.write_u8(self.feat)?; + } + if status & 0x10 > 0 { + writer.write_u64(self.ttl)?; + } + Ok(()) + } +} + +impl Readable for SlateOptFields { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let num_parts = if status & 0x01 > 0 { + reader.read_u8()? + } else { + 2 + }; + let amt = if status & 0x02 > 0 { + reader.read_u64()? + } else { + 0 + }; + let fee = if status & 0x04 > 0 { + FeeFields::read(reader)? + } else { + FeeFields::zero() + }; + let feat = if status & 0x08 > 0 { + reader.read_u8()? + } else { + 0 + }; + let ttl = if status & 0x10 > 0 { + reader.read_u64()? + } else { + 0 + }; + Ok(SlateOptFields { + num_parts, + amt, + fee, + feat, + ttl, + }) + } +} + +struct SigsWrap(Vec); +struct SigsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for SigsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(self.0.len() as u8)?; + for s in self.0.iter() { + //0 means part sig is not yet included + //1 bit set means part sig included + //2 bit set means tau_x included + //4 bit set means tau_one included + //8 bit set means tau_two included + let mut optional = s.part.is_some() as u8; + if s.tau_x.is_some() { + optional |= 2; + } + if s.tau_one.is_some() { + optional |= 4; + } + if s.tau_two.is_some() { + optional |= 8; + } + writer.write_u8(optional)?; + s.xs.write(writer)?; + s.nonce.write(writer)?; + if let Some(s) = s.part { + s.write(writer)?; + } + if let Some(tx) = s.tau_x.as_ref() { + writer.write_fixed_bytes(tx.0)?; + } + if let Some(to) = s.tau_one.as_ref() { + to.write(writer)?; + } + if let Some(tt) = s.tau_two.as_ref() { + tt.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for SigsWrap { + fn read(reader: &mut R) -> Result { + let sigs_len = reader.read_u8()?; + let sigs = { + let mut ret = vec![]; + for _ in 0..sigs_len as usize { + let has_optional = reader.read_u8()?; + let has_partial = has_optional & 1 != 0; + let has_tau_x = has_optional & 2 != 0; + let has_tau_one = has_optional & 4 != 0; + let has_tau_two = has_optional & 8 != 0; + let c = ParticipantDataV5 { + xs: PublicKey::read(reader)?, + nonce: PublicKey::read(reader)?, + part: match has_partial { + true => Some(Signature::read(reader)?), + false => None, + }, + tau_x: match has_tau_x { + true => { + let secp = static_secp_instance(); + let secp = secp.lock(); + let key_bytes = reader.read_fixed_bytes(32)?; + Some( + SecretKey::from_slice(&secp, &key_bytes) + .map_err(|_| grin_ser::Error::CorruptedData)?, + ) + } + false => None, + }, + tau_one: match has_tau_one { + true => Some(PublicKey::read(reader)?), + false => None, + }, + tau_two: match has_tau_two { + true => Some(PublicKey::read(reader)?), + false => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(SigsWrap(sigs)) + } +} + +/// Serialization of optional structs +struct SlateOptStructsRef<'a> { + /// coms, default none + pub coms: &'a Option>, + ///// proof, default none + pub proof: &'a Option, +} + +/// Serialization of optional structs +struct SlateOptStructs { + /// coms, default none + pub coms: Option>, + /// proof, default none + pub proof: Option, +} + +impl<'a> Writeable for SlateOptStructsRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional structs are serialized + // 0 0 0 0 0 0 1 1 + // p c + let mut status = 0u8; + if self.coms.is_some() { + status |= 0x01 + }; + if self.proof.is_some() { + status |= 0x02 + }; + writer.write_u8(status)?; + if let Some(c) = self.coms { + ComsWrapRef(&c).write(writer)?; + } + if let Some(p) = self.proof { + ProofWrapRef(&p).write(writer)?; + } + Ok(()) + } +} + +impl Readable for SlateOptStructs { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let coms = if status & 0x01 > 0 { + Some(ComsWrap::read(reader)?.0) + } else { + None + }; + let proof = if status & 0x02 > 0 { + Some(ProofWrap::read(reader)?.0) + } else { + None + }; + Ok(SlateOptStructs { coms, proof }) + } +} + +struct ComsWrap(Vec); +struct ComsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for ComsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u16(self.0.len() as u16)?; + for o in self.0.iter() { + //0 means input + //1 means output with proof + if o.p.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + OutputFeatures::from(o.f).write(writer)?; + o.c.write(writer)?; + if let Some(p) = o.p { + p.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for ComsWrap { + fn read(reader: &mut R) -> Result { + let coms_len = reader.read_u16()?; + let coms = { + let mut ret = vec![]; + for _ in 0..coms_len as usize { + let is_output = reader.read_u8()?; + let c = CommitsV5 { + f: OutputFeatures::read(reader)?.into(), + c: Commitment::read(reader)?, + p: match is_output { + 1 => Some(RangeProof::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(ComsWrap(coms)) + } +} + +struct ProofWrap(PaymentInfoV5); +struct ProofWrapRef<'a>(&'a PaymentInfoV5); + +impl<'a> Writeable for ProofWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_fixed_bytes(self.0.saddr.to_bytes())?; + writer.write_fixed_bytes(self.0.raddr.to_bytes())?; + match self.0.rsig { + Some(s) => { + writer.write_u8(1)?; + writer.write_fixed_bytes(&s.to_bytes().to_vec())?; + } + None => writer.write_u8(0)?, + } + Ok(()) + } +} + +impl Readable for ProofWrap { + fn read(reader: &mut R) -> Result { + let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let rsig = match reader.read_u8()? { + 0 => None, + 1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()), + }; + Ok(ProofWrap(PaymentInfoV5 { saddr, raddr, rsig })) + } +} + +#[derive(Debug, Clone)] +pub struct SlateV5Bin(pub SlateV5); + +impl From for SlateV5Bin { + fn from(slate: SlateV5) -> SlateV5Bin { + SlateV5Bin(slate) + } +} + +impl From for SlateV5 { + fn from(slate: SlateV5Bin) -> SlateV5 { + slate.0 + } +} + +impl serde::Serialize for SlateV5Bin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(5), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlateV5Bin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlateV5BinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlateV5BinVisitor { + type Value = SlateV5Bin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary V5 slate") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = grin_ser::deserialize(&mut reader, grin_ser::ProtocolVersion(4)) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + deserializer.deserialize_bytes(SlateV5BinVisitor) + } +} + +impl Writeable for SlateV5Bin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let v5 = &self.0; + writer.write_u16(v5.ver.version)?; + writer.write_u16(v5.ver.block_header_version)?; + (UuidWrap(v5.id)).write(writer)?; + v5.sta.write(writer)?; + v5.off.write(writer)?; + SlateOptFields { + num_parts: v5.num_parts, + amt: v5.amt, + fee: v5.fee, + feat: v5.feat, + ttl: v5.ttl, + } + .write(writer)?; + (SigsWrapRef(&v5.sigs)).write(writer)?; + SlateOptStructsRef { + coms: &v5.coms, + proof: &v5.proof, + } + .write(writer)?; + // Write lock height for height locked kernels + if v5.feat == 2 { + let lock_hgt = match &v5.feat_args { + Some(l) => l.lock_hgt, + None => 0, + }; + writer.write_u64(lock_hgt)?; + } + Ok(()) + } +} + +impl Readable for SlateV5Bin { + fn read(reader: &mut R) -> Result { + let ver = VersionCompatInfoV5 { + version: reader.read_u16()?, + block_header_version: reader.read_u16()?, + }; + let id = UuidWrap::read(reader)?.0; + let sta = SlateStateV5::read(reader)?; + let off = BlindingFactor::read(reader)?; + + let opts = SlateOptFields::read(reader)?; + let sigs = SigsWrap::read(reader)?.0; + let opt_structs = SlateOptStructs::read(reader)?; + + let feat_args = if opts.feat == 2 { + Some(KernelFeaturesArgsV5 { + lock_hgt: reader.read_u64()?, + }) + } else { + None + }; + + Ok(SlateV5Bin(SlateV5 { + ver, + id, + sta, + off, + num_parts: opts.num_parts, + amt: opts.amt, + fee: opts.fee, + feat: opts.feat, + ttl: opts.ttl, + sigs, + coms: opt_structs.coms, + proof: opt_structs.proof, + feat_args, + })) + } +} + +#[test] +fn slate_v5_serialize_deserialize() { + use crate::grin_util::from_hex; + use crate::grin_util::secp::key::PublicKey; + use crate::Slate; + use grin_wallet_util::grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_wallet_util::grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + set_local_chain_type(ChainTypes::Mainnet); + let slate = Slate::blank(1, false); + let mut v5 = SlateV5::from(slate); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + // add some sig data + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + let part = ParticipantDataV5 { + xs, + nonce, + part: None, + tau_x: None, + tau_one: None, + tau_two: None, + }; + let part2 = ParticipantDataV5 { + xs, + nonce, + part: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + tau_x: None, + tau_one: None, + tau_two: None, + }; + v5.sigs.push(part.clone()); + v5.sigs.push(part2); + v5.sigs.push(part); + + // add some random commit data + let com1 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v5.coms = Some(coms); + v5.amt = 234324899824; + v5.feat = 1; + v5.num_parts = 2; + v5.feat_args = Some(KernelFeaturesArgsV5 { lock_hgt: 23092039 }); + let v5_1 = v5.clone(); + let v5_1_copy = v5.clone(); + + let v5_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v5_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v5_2 = b4_bin_2.0.clone(); + assert_eq!(v5_1.ver, v5_2.ver); + assert_eq!(v5_1.id, v5_2.id); + assert_eq!(v5_1.amt, v5_2.amt); + assert_eq!(v5_1.fee, v5_2.fee); + let v5_2_coms = v5_2.coms.as_ref().unwrap().clone(); + for (i, c) in v5_1.coms.unwrap().iter().enumerate() { + assert_eq!(c.f, v5_2_coms[i].f); + assert_eq!(c.c, v5_2_coms[i].c); + assert_eq!(c.p, v5_2_coms[i].p); + } + assert_eq!(v5_1.sigs, v5_2.sigs); + assert_eq!(v5_1.proof, v5_2.proof); + + // Include Payment proof, remove coms to mix it up a bit + let mut v5 = v5_1_copy; + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(&b).unwrap(); + v5.proof = Some(PaymentInfoV5 { + raddr: d_pkey.clone(), + saddr: d_pkey.clone(), + rsig: None, + }); + v5.coms = None; + let v5_1 = v5.clone(); + let v5_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v5_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v5_2 = b4_bin_2.0.clone(); + assert_eq!(v5_1.ver, v5_2.ver); + assert_eq!(v5_1.id, v5_2.id); + assert_eq!(v5_1.amt, v5_2.amt); + assert_eq!(v5_1.fee, v5_2.fee); + assert!(v5_1.coms.is_none()); + assert_eq!(v5_1.sigs, v5_2.sigs); + assert_eq!(v5_1.proof, v5_2.proof); +} diff --git a/libwallet/src/slatepack/packer.rs b/libwallet/src/slatepack/packer.rs index 9ef31ccaa..c26fc8fba 100644 --- a/libwallet/src/slatepack/packer.rs +++ b/libwallet/src/slatepack/packer.rs @@ -17,8 +17,8 @@ use std::str; use super::armor::HEADER; use crate::{ - slatepack, Slate, SlateVersion, Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin, - VersionedBinSlate, VersionedSlate, + slatepack, Slate, Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin, VersionedBinSlate, + VersionedSlate, }; use crate::{Error, ErrorKind}; @@ -94,7 +94,7 @@ impl<'a> Slatepacker<'a> { /// Create slatepack from slate and args pub fn create_slatepack(&self, slate: &Slate) -> Result { - let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + let out_slate = VersionedSlate::into_version(slate.clone(), slate.version())?; let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| ErrorKind::SlatepackSer)?; let mut slatepack = Slatepack::default(); diff --git a/libwallet/src/slatepack/types.rs b/libwallet/src/slatepack/types.rs index 52375da0e..f56ecc9c1 100644 --- a/libwallet/src/slatepack/types.rs +++ b/libwallet/src/slatepack/types.rs @@ -20,7 +20,7 @@ use x25519_dalek::StaticSecret; use crate::dalek_ser; use crate::grin_core::ser::{self, Readable, Reader, Writeable, Writer}; -use crate::{Error, ErrorKind}; +use crate::{Error, ErrorKind, CURRENT_SLATE_VERSION}; use grin_wallet_util::byte_ser; use super::SlatepackAddress; @@ -260,8 +260,12 @@ impl serde::Serialize for SlatepackBin { S: serde::Serializer, { let mut vec = vec![]; - ser::serialize(&mut vec, ser::ProtocolVersion(4), self) - .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + ser::serialize( + &mut vec, + ser::ProtocolVersion(CURRENT_SLATE_VERSION as u32), + self, + ) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; serializer.serialize_bytes(&vec) } } @@ -285,8 +289,11 @@ impl<'de> serde::Deserialize<'de> for SlatepackBin { E: serde::de::Error, { let mut reader = std::io::Cursor::new(value.to_vec()); - let s = ser::deserialize(&mut reader, ser::ProtocolVersion(4)) - .map_err(|err| serde::de::Error::custom(err.to_string()))?; + let s = ser::deserialize( + &mut reader, + ser::ProtocolVersion(CURRENT_SLATE_VERSION as u32), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; Ok(s) } } @@ -488,8 +495,12 @@ impl serde::Serialize for SlatepackEncMetadataBin { S: serde::Serializer, { let mut vec = vec![]; - ser::serialize(&mut vec, ser::ProtocolVersion(4), self) - .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + ser::serialize( + &mut vec, + ser::ProtocolVersion(CURRENT_SLATE_VERSION as u32), + self, + ) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; serializer.serialize_bytes(&vec) } } @@ -513,8 +524,11 @@ impl<'de> serde::Deserialize<'de> for SlatepackEncMetadataBin { E: serde::de::Error, { let mut reader = std::io::Cursor::new(value.to_vec()); - let s = ser::deserialize(&mut reader, ser::ProtocolVersion(4)) - .map_err(|err| serde::de::Error::custom(err.to_string()))?; + let s = ser::deserialize( + &mut reader, + ser::ProtocolVersion(CURRENT_SLATE_VERSION as u32), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; Ok(s) } } diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs index 7cbe0e3be..784526a9f 100644 --- a/tests/owner_v3_lifecycle.rs +++ b/tests/owner_v3_lifecycle.rs @@ -405,12 +405,13 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { //16) Finalize the invoice tx (to foreign api) // (Tests that foreign API on same port also has its stored mask updated) + let v = slate.version(); let req = serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": "finalize_tx", "params": { - "slate": VersionedSlate::into_version(slate, SlateVersion::V4)?, + "slate": VersionedSlate::into_version(slate, v)?, } }); let res = From ac61abe36d517e78dcf8ce22fae8e4e9e92d9b1a Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 30 Jun 2021 20:28:08 +0000 Subject: [PATCH 03/17] Build multisig input if the output is multisig Add functionality to build a multisig input if the output is multisig --- libwallet/src/api_impl/owner.rs | 145 ++++++++++++++++++++++++++++ libwallet/src/internal/scan.rs | 1 + libwallet/src/internal/selection.rs | 8 ++ libwallet/src/internal/updater.rs | 1 + libwallet/src/slate.rs | 1 + libwallet/src/types.rs | 2 + 6 files changed, 158 insertions(+) diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 67092d391..24d506a9e 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -728,6 +728,151 @@ where Ok(ret_slate) } +/// Perform initiator's step 1 + 2 of the multisig bulletproof to create +/// tau_x, tau_one, tau_two keys +pub fn process_multisig_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = w.keychain(keychain_mask)?; + let mut context = w.get_private_context(keychain_mask, slate.id.as_bytes())?; + let mut ret_slate = slate.clone(); + + let secp = keychain.secp(); + let key_id = slate.create_multisig_id(); + let (_, pub_nonce) = context.get_public_keys(secp); + let oth_part_data = slate + .participant_data + .iter() + .find(|d| d.public_nonce != pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant data".into(), + )))?; + + let oth_part_commit = + oth_part_data + .part_commit + .clone() + .ok_or(Error::from(ErrorKind::Commit( + "missing other partial commit".into(), + )))?; + let partial_commit = keychain.commit(slate.amount, &key_id, SwitchCommitmentType::Regular)?; + let commit_sum = secp.commit_sum(vec![partial_commit, oth_part_commit], vec![])?; + + let common_nonce = context.create_common_nonce(secp, &oth_part_data.public_nonce)?; + + // calculate initiator's tau_one and tau_two public keys for the multisig bulletproof + context.tau_one = Some(PublicKey::new()); + context.tau_two = Some(PublicKey::new()); + let _ = proof::create_multisig( + &keychain, + &proof::ProofBuilder::new(&keychain), + slate.amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + None, + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit_sum.clone()], + 1, + None, + )?; + + if oth_part_data.tau_one.is_none() || oth_part_data.tau_two.is_none() { + return Err(ErrorKind::GenericError( + "missing other participant's tau public key(s)".into(), + ) + .into()); + } + + // calculate tau_one_sum and tau_two_sum + let mut tau_one_sum = Some(PublicKey::from_combination( + secp, + vec![ + context.tau_one.as_ref().unwrap(), + oth_part_data.tau_one.as_ref().unwrap(), + ], + )?); + let mut tau_two_sum = Some(PublicKey::from_combination( + secp, + vec![ + context.tau_two.as_ref().unwrap(), + oth_part_data.tau_two.as_ref().unwrap(), + ], + )?); + + // calculate initiator's tau_x secret key for the multisig bulletproof + context.tau_x = Some(SecretKey::new(secp, &mut thread_rng())); + let _ = proof::create_multisig( + &keychain, + &proof::ProofBuilder::new(&keychain), + slate.amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + context.tau_x.as_mut(), + tau_one_sum.as_mut(), + tau_two_sum.as_mut(), + &[commit_sum], + 2, + None, + )?; + + let part_data = ret_slate + .participant_data + .iter_mut() + .find(|d| d.public_nonce == pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing local participant data".into(), + )))?; + + part_data.tau_x = context.tau_x.clone(); + part_data.tau_one = context.tau_one.clone(); + part_data.tau_two = context.tau_two.clone(); + + context.tau_one = tau_one_sum; + context.tau_two = tau_two_sum; + + // Don't calculate partial excess signature, wait for tau_x from the receiver + // After receiving tau_x, compute the final bulletproof over the shared output + // + // Then, calculate the partial excess signature, add to receiver's signature, + // and finalize the multisig transaction + + // Save the multisig output and context in our DB + { + let height = w.last_confirmed_height()?; + let mut batch = w.batch(keychain_mask)?; + let commit = Some(commit_sum.0.to_vec().to_hex()); + batch.save(OutputData { + root_key_id: key_id.clone(), + key_id: key_id.clone(), + mmr_index: None, + n_child: key_id.to_path().last_path_index(), + commit, + value: slate.amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: 0, + is_coinbase: false, + is_multisig: true, + tx_log_entry: None, + })?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + ret_slate.state = SlateState::Multisig3; + Ok(ret_slate) +} + /// Lock sender outputs pub fn tx_lock_outputs<'a, T: ?Sized, C, K>( w: &mut T, diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs index 25c254355..ef0354e13 100644 --- a/libwallet/src/internal/scan.rs +++ b/libwallet/src/internal/scan.rs @@ -274,6 +274,7 @@ where height: output.height, lock_height: output.lock_height, is_coinbase: output.is_coinbase, + is_multisig: output.is_multisig, tx_log_entry: Some(log_id), }); diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 7c66b9672..c33879e7e 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -219,6 +219,7 @@ where height: height, lock_height: 0, is_coinbase: false, + is_multisig: slate.is_multisig(), tx_log_entry: Some(log_id), })?; } @@ -297,6 +298,7 @@ where height: height, lock_height: 0, is_coinbase: false, + is_multisig: slate.is_multisig(), tx_log_entry: Some(log_id), })?; batch.save_tx_log_entry(t.clone(), &parent_key_id)?; @@ -672,6 +674,12 @@ where if let Some(i) = input { if i.is_coinbase { parts.push(build::coinbase_input(*value, i.key_id.clone())); + } else if i.is_multisig { + let commit_str = i.commit.ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output commitment".into(), + )))?; + let commit = pedersen::Commitment::from_hex(&commit_str)?; + parts.push(build::multisig_input(*value, i.key_id.clone(), commit)); } else { parts.push(build::input(*value, i.key_id.clone())); } diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index da2209e3f..e2ebf9f20 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -613,6 +613,7 @@ where height: height, lock_height: lock_height, is_coinbase: true, + is_multisig: false, tx_log_entry: None, })?; batch.commit()?; diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 061225e2e..8ba0843e5 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -1567,6 +1567,7 @@ impl From for OutputFeatures { impl From for OutputFeatures { fn from(of: OutputFeaturesV5) -> OutputFeatures { match of.0 { + 2 => OutputFeatures::Multisig, 1 => OutputFeatures::Coinbase, 0 | _ => OutputFeatures::Plain, } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 7d0ebf07a..f65d8e344 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -426,6 +426,8 @@ pub struct OutputData { pub lock_height: u64, /// Is this a coinbase output? Is it subject to coinbase locktime? pub is_coinbase: bool, + /// Is this a multisig output? + pub is_multisig: bool, /// Optional corresponding internal entry in tx entry log pub tx_log_entry: Option, } From 6692ca2e30629b68eb31759ead36cd54dba439c9 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 21 Apr 2021 20:59:34 +0000 Subject: [PATCH 04/17] Add multisig and atomic types to V5 slate Add V5 slate types for multisig and atomic swap transaction flows --- libwallet/src/slate.rs | 68 ++++++++++++++++++++++++++ libwallet/src/slate_versions/ser.rs | 16 ++++++ libwallet/src/slate_versions/v5.rs | 34 +++++++++++++ libwallet/src/slate_versions/v5_bin.rs | 62 +++++++++++++++++++---- libwallet/src/types.rs | 32 ++++++++++++ libwallet/tests/libwallet.rs | 10 ++++ 6 files changed, 213 insertions(+), 9 deletions(-) diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 8ba0843e5..57dddf8e7 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -64,8 +64,12 @@ pub struct ParticipantData { pub public_blind_excess: PublicKey, /// Public key corresponding to private nonce pub public_nonce: PublicKey, + /// Public key corresponding to the atomic secret + pub public_atomic: Option, /// Public partial signature pub part_sig: Option, + /// Public partial commitment to multisig output value + pub part_commit: Option, /// Tau X key for multiparty output rangeproof pub tau_x: Option, /// Tau one partial public key for multiparty output rangeproof @@ -160,6 +164,22 @@ pub enum SlateState { Invoice2, /// Invoice flow, ready for tranasction posting Invoice3, + /// Multisig flow, freshly init + Multisig1, + ///Multisig flow, step 1 proof build + Multisig2, + /// Multisig flow, step 2 proof build + Multisig3, + /// Multisig flow, final proof step + Multisig4, + /// Atomic flow, freshly init + Atomic1, + /// Atomic flow, return journey + Atomic2, + /// Atomic flow, partial signature from initiator + Atomic3, + /// Atomic flow, ready for transaction posting + Atomic4, } impl fmt::Display for SlateState { @@ -172,6 +192,14 @@ impl fmt::Display for SlateState { SlateState::Invoice1 => "I1", SlateState::Invoice2 => "I2", SlateState::Invoice3 => "I3", + SlateState::Multisig1 => "M1", + SlateState::Multisig2 => "M2", + SlateState::Multisig3 => "M3", + SlateState::Multisig4 => "M4", + SlateState::Atomic1 => "A1", + SlateState::Atomic2 => "A2", + SlateState::Atomic3 => "A3", + SlateState::Atomic4 => "A4", }; write!(f, "{}", res) } @@ -499,6 +527,7 @@ impl Slate { let pub_key = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?; let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; let mut part_sig = part_sig; + let part_commit = context.partial_commit.clone(); // Remove if already here and replace self.participant_data = self @@ -519,7 +548,9 @@ impl Slate { self.participant_data.push(ParticipantData { public_blind_excess: pub_key, public_nonce: pub_nonce, + public_atomic: None, part_sig: part_sig, + part_commit, tau_x: None, tau_one: None, tau_two: None, @@ -976,7 +1007,9 @@ impl From<&ParticipantData> for ParticipantDataV4 { let ParticipantData { public_blind_excess, public_nonce, + public_atomic: _, part_sig, + part_commit: _, tau_x: _, tau_one: _, tau_two: _, @@ -997,21 +1030,27 @@ impl From<&ParticipantData> for ParticipantDataV5 { let ParticipantData { public_blind_excess, public_nonce, + public_atomic, part_sig, + part_commit, tau_x, tau_one, tau_two, } = data; let public_blind_excess = *public_blind_excess; let public_nonce = *public_nonce; + let public_atomic = *public_atomic; let part_sig = *part_sig; + let part_commit = *part_commit; let tau_x = tau_x.clone(); let tau_one = tau_one.clone(); let tau_two = tau_two.clone(); ParticipantDataV5 { xs: public_blind_excess, nonce: public_nonce, + atomic: public_atomic, part: part_sig, + part_commit, tau_x, tau_one, tau_two, @@ -1029,6 +1068,7 @@ impl From<&SlateState> for SlateStateV4 { SlateState::Invoice1 => SlateStateV4::Invoice1, SlateState::Invoice2 => SlateStateV4::Invoice2, SlateState::Invoice3 => SlateStateV4::Invoice3, + _ => SlateStateV4::Unknown, } } } @@ -1043,6 +1083,14 @@ impl From<&SlateState> for SlateStateV5 { SlateState::Invoice1 => SlateStateV5::Invoice1, SlateState::Invoice2 => SlateStateV5::Invoice2, SlateState::Invoice3 => SlateStateV5::Invoice3, + SlateState::Atomic1 => SlateStateV5::Atomic1, + SlateState::Atomic2 => SlateStateV5::Atomic2, + SlateState::Atomic3 => SlateStateV5::Atomic3, + SlateState::Atomic4 => SlateStateV5::Atomic4, + SlateState::Multisig1 => SlateStateV5::Multisig1, + SlateState::Multisig2 => SlateStateV5::Multisig2, + SlateState::Multisig3 => SlateStateV5::Multisig3, + SlateState::Multisig4 => SlateStateV5::Multisig4, } } } @@ -1258,7 +1306,9 @@ pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { calc_slate.participant_data.push(ParticipantData { public_blind_excess: d.xs, public_nonce: d.nonce, + public_atomic: None, part_sig: d.part, + part_commit: None, tau_x: None, tau_one: None, tau_two: None, @@ -1327,7 +1377,9 @@ pub fn tx_from_slate_v5(slate: &SlateV5) -> Option { calc_slate.participant_data.push(ParticipantData { public_blind_excess: d.xs, public_nonce: d.nonce, + public_atomic: d.atomic, part_sig: d.part, + part_commit: d.part_commit, tau_x: d.tau_x.clone(), tau_one: d.tau_one.clone(), tau_two: d.tau_two.clone(), @@ -1410,7 +1462,9 @@ impl From<&ParticipantDataV4> for ParticipantData { ParticipantData { public_blind_excess, public_nonce, + public_atomic: None, part_sig, + part_commit: None, tau_x: None, tau_one: None, tau_two: None, @@ -1424,20 +1478,26 @@ impl From<&ParticipantDataV5> for ParticipantData { xs: public_blind_excess, nonce: public_nonce, part: part_sig, + atomic: public_atomic, + part_commit, tau_x, tau_one, tau_two, } = data; let public_blind_excess = *public_blind_excess; let public_nonce = *public_nonce; + let public_atomic = *public_atomic; let part_sig = *part_sig; + let part_commit = *part_commit; let tau_x = tau_x.clone(); let tau_one = tau_one.clone(); let tau_two = tau_two.clone(); ParticipantData { public_blind_excess, public_nonce, + public_atomic, part_sig, + part_commit, tau_x, tau_one, tau_two, @@ -1485,6 +1545,14 @@ impl From<&SlateStateV5> for SlateState { SlateStateV5::Invoice1 => SlateState::Invoice1, SlateStateV5::Invoice2 => SlateState::Invoice2, SlateStateV5::Invoice3 => SlateState::Invoice3, + SlateStateV5::Multisig1 => SlateState::Multisig1, + SlateStateV5::Multisig2 => SlateState::Multisig2, + SlateStateV5::Multisig3 => SlateState::Multisig3, + SlateStateV5::Multisig4 => SlateState::Multisig4, + SlateStateV5::Atomic1 => SlateState::Atomic1, + SlateStateV5::Atomic2 => SlateState::Atomic2, + SlateStateV5::Atomic3 => SlateState::Atomic3, + SlateStateV5::Atomic4 => SlateState::Atomic4, } } } diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 7e28da0f0..e700e88e9 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -621,6 +621,14 @@ pub mod slate_state_v5 { SlateStateV5::Invoice1 => "I1", SlateStateV5::Invoice2 => "I2", SlateStateV5::Invoice3 => "I3", + SlateStateV5::Atomic1 => "A1", + SlateStateV5::Atomic2 => "A2", + SlateStateV5::Atomic3 => "A3", + SlateStateV5::Atomic4 => "A4", + SlateStateV5::Multisig1 => "M1", + SlateStateV5::Multisig2 => "M2", + SlateStateV5::Multisig3 => "M3", + SlateStateV5::Multisig4 => "M4", }; serializer.serialize_str(label) } @@ -639,6 +647,14 @@ pub mod slate_state_v5 { "I1" => SlateStateV5::Invoice1, "I2" => SlateStateV5::Invoice2, "I3" => SlateStateV5::Invoice3, + "A1" => SlateStateV5::Atomic1, + "A2" => SlateStateV5::Atomic2, + "A3" => SlateStateV5::Atomic3, + "A4" => SlateStateV5::Atomic4, + "M1" => SlateStateV5::Multisig1, + "M2" => SlateStateV5::Multisig2, + "M3" => SlateStateV5::Multisig3, + "M4" => SlateStateV5::Multisig4, _ => return Err(Error::custom("Invalid Slate state")), }; Ok(retval) diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs index ebaf3f210..756764a41 100644 --- a/libwallet/src/slate_versions/v5.rs +++ b/libwallet/src/slate_versions/v5.rs @@ -147,6 +147,22 @@ pub enum SlateStateV5 { Invoice2, /// Invoice flow, ready for tranasction posting Invoice3, + /// Multisig flow, freshly init + Multisig1, + ///Multisig flow, step 1 proof build + Multisig2, + /// Multisig flow, step 2 proof build + Multisig3, + /// Multisig flow, final proof step + Multisig4, + /// Atomic flow, freshly init + Atomic1, + ///Atomic flow, return journey + Atomic2, + /// Atomic flow, partial signature from initiator + Atomic3, + /// Atomic flow, ready for tranasction posting + Atomic4, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -172,11 +188,21 @@ pub struct ParticipantDataV5 { /// Public key corresponding to private nonce #[serde(with = "secp_ser::pubkey_serde")] pub nonce: PublicKey, + /// Public key corresponding to atomic secret + #[serde(default = "default_atomic_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_pubkey_serde")] + pub atomic: Option, /// Public partial signature #[serde(default = "default_part_sig_none")] #[serde(skip_serializing_if = "Option::is_none")] #[serde(with = "secp_ser::option_sig_serde")] pub part: Option, + /// Public partial commitment to multisig output value + #[serde(default = "default_part_com_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_commitment_serde")] + pub part_commit: Option, /// Tau X key for shared outputs #[serde(default = "default_tau_x_none")] #[serde(skip_serializing_if = "Option::is_none")] @@ -193,10 +219,18 @@ pub struct ParticipantDataV5 { pub tau_two: Option, } +fn default_atomic_none() -> Option { + None +} + fn default_part_sig_none() -> Option { None } +fn default_part_com_none() -> Option { + None +} + fn default_tau_x_none() -> Option { None } diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs index 00ba8f21a..578322323 100644 --- a/libwallet/src/slate_versions/v5_bin.rs +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -42,6 +42,14 @@ impl Writeable for SlateStateV5 { SlateStateV5::Invoice1 => 4, SlateStateV5::Invoice2 => 5, SlateStateV5::Invoice3 => 6, + SlateStateV5::Multisig1 => 7, + SlateStateV5::Multisig2 => 8, + SlateStateV5::Multisig3 => 9, + SlateStateV5::Multisig4 => 10, + SlateStateV5::Atomic1 => 11, + SlateStateV5::Atomic2 => 12, + SlateStateV5::Atomic3 => 13, + SlateStateV5::Atomic4 => 14, }; writer.write_u8(b) } @@ -58,6 +66,14 @@ impl Readable for SlateStateV5 { 4 => SlateStateV5::Invoice1, 5 => SlateStateV5::Invoice2, 6 => SlateStateV5::Invoice3, + 7 => SlateStateV5::Multisig1, + 8 => SlateStateV5::Multisig2, + 9 => SlateStateV5::Multisig3, + 10 => SlateStateV5::Multisig4, + 11 => SlateStateV5::Atomic1, + 12 => SlateStateV5::Atomic2, + 13 => SlateStateV5::Atomic3, + 14 => SlateStateV5::Atomic4, _ => SlateStateV5::Unknown, }; Ok(sta) @@ -185,25 +201,39 @@ impl<'a> Writeable for SigsWrapRef<'a> { for s in self.0.iter() { //0 means part sig is not yet included //1 bit set means part sig included - //2 bit set means tau_x included - //4 bit set means tau_one included - //8 bit set means tau_two included + //2 bit set means atomic included + //4 bit set means part commit included + //8 bit set means tau_x included + //16 bit set means tau_one included + //32 bit set means tau_two included let mut optional = s.part.is_some() as u8; - if s.tau_x.is_some() { + if s.atomic.is_some() { optional |= 2; } - if s.tau_one.is_some() { + if s.part_commit.is_some() { optional |= 4; } - if s.tau_two.is_some() { + if s.tau_x.is_some() { optional |= 8; } + if s.tau_one.is_some() { + optional |= 16; + } + if s.tau_two.is_some() { + optional |= 32; + } writer.write_u8(optional)?; s.xs.write(writer)?; s.nonce.write(writer)?; + if let Some(a) = s.atomic { + a.write(writer)?; + } if let Some(s) = s.part { s.write(writer)?; } + if let Some(c) = s.part_commit { + c.write(writer)?; + } if let Some(tx) = s.tau_x.as_ref() { writer.write_fixed_bytes(tx.0)?; } @@ -225,17 +255,27 @@ impl Readable for SigsWrap { let mut ret = vec![]; for _ in 0..sigs_len as usize { let has_optional = reader.read_u8()?; + let has_atomic = has_optional & 2 != 0; let has_partial = has_optional & 1 != 0; - let has_tau_x = has_optional & 2 != 0; - let has_tau_one = has_optional & 4 != 0; - let has_tau_two = has_optional & 8 != 0; + let has_part_com = has_optional & 4 != 0; + let has_tau_x = has_optional & 8 != 0; + let has_tau_one = has_optional & 16 != 0; + let has_tau_two = has_optional & 32 != 0; let c = ParticipantDataV5 { xs: PublicKey::read(reader)?, nonce: PublicKey::read(reader)?, + atomic: match has_atomic { + true => Some(PublicKey::read(reader)?), + false => None, + }, part: match has_partial { true => Some(Signature::read(reader)?), false => None, }, + part_commit: match has_part_com { + true => Some(Commitment::read(reader)?), + false => None, + }, tau_x: match has_tau_x { true => { let secp = static_secp_instance(); @@ -550,7 +590,9 @@ fn slate_v5_serialize_deserialize() { let part = ParticipantDataV5 { xs, nonce, + atomic: None, part: None, + part_commit: None, tau_x: None, tau_one: None, tau_two: None, @@ -558,7 +600,9 @@ fn slate_v5_serialize_deserialize() { let part2 = ParticipantDataV5 { xs, nonce, + atomic: None, part: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + part_commit: None, tau_x: None, tau_one: None, tau_two: None, diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index f65d8e344..4929e561c 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -554,6 +554,8 @@ pub struct Context { pub initial_sec_key: SecretKey, /// as above pub initial_sec_nonce: SecretKey, + /// Secret key (of which public is shared, atomic swap only) + pub sec_atomic: Option, /// store my outputs + amounts between invocations /// Id, mmr_index (if known), amount pub output_ids: Vec<(Identifier, Option, u64)>, @@ -573,6 +575,8 @@ pub struct Context { /// for invoice I2 Only, store the tx excess so we can /// remove it from the slate on return pub calculated_excess: Option, + /// For multisig only, store the partial commitment to the output value + pub partial_commit: Option, } impl Context { @@ -615,6 +619,7 @@ impl Context { sec_nonce: sec_nonce.clone(), initial_sec_key: sec_key, initial_sec_nonce: sec_nonce, + sec_atomic: None, input_ids: vec![], output_ids: vec![], amount: 0, @@ -622,6 +627,7 @@ impl Context { payment_proof_derivation_index: None, late_lock_args: None, calculated_excess: None, + partial_commit: None, } } } @@ -662,6 +668,32 @@ impl Context { PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(), ) } + + /// Create an atomic secret key + pub fn create_atomic_secret(&mut self, secret: SecretKey) { + self.sec_atomic = Some(secret); + } + + /// Set an atomic secret key + pub fn set_atomic_secret(&mut self, secret: SecretKey) { + self.sec_atomic = Some(secret); + } + + /// Get the atomic secret key + pub fn get_secret_atomic_secret(&self) -> Option<&SecretKey> { + match &self.sec_atomic { + Some(n) => Some(n), + None => None, + } + } + + /// Get the atomic public key + pub fn get_public_atomic(&self, secp: &Secp256k1) -> Result, Error> { + match &self.sec_atomic { + Some(a) => Ok(Some(PublicKey::from_secret_key(secp, a)?)), + None => Ok(None), + } + } } impl ser::Writeable for Context { diff --git a/libwallet/tests/libwallet.rs b/libwallet/tests/libwallet.rs index bd14a95a6..bf7ce3d22 100644 --- a/libwallet/tests/libwallet.rs +++ b/libwallet/tests/libwallet.rs @@ -118,6 +118,7 @@ fn aggsig_sender_receiver_interaction() { &keychain.secp(), &rx_cx.sec_key, &rx_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, @@ -135,6 +136,7 @@ fn aggsig_sender_receiver_interaction() { &keychain.secp(), &rx_sig_part, &pub_nonce_sum, + None, &receiver_pub_excess, Some(&pub_key_sum), &msg, @@ -150,6 +152,7 @@ fn aggsig_sender_receiver_interaction() { &keychain.secp(), &s_cx.sec_key, &s_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, @@ -167,6 +170,7 @@ fn aggsig_sender_receiver_interaction() { &keychain.secp(), &sender_sig_part, &pub_nonce_sum, + None, &sender_pub_excess, Some(&pub_key_sum), &msg, @@ -183,6 +187,7 @@ fn aggsig_sender_receiver_interaction() { &keychain.secp(), &rx_cx.sec_key, &rx_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, @@ -336,6 +341,7 @@ fn aggsig_sender_receiver_interaction_offset() { &keychain.secp(), &rx_cx.sec_key, &rx_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, @@ -353,6 +359,7 @@ fn aggsig_sender_receiver_interaction_offset() { &keychain.secp(), &sig_part, &pub_nonce_sum, + None, &receiver_pub_excess, Some(&pub_key_sum), &msg, @@ -368,6 +375,7 @@ fn aggsig_sender_receiver_interaction_offset() { &keychain.secp(), &s_cx.sec_key, &s_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, @@ -385,6 +393,7 @@ fn aggsig_sender_receiver_interaction_offset() { &keychain.secp(), &sender_sig_part, &pub_nonce_sum, + None, &sender_pub_excess, Some(&pub_key_sum), &msg, @@ -400,6 +409,7 @@ fn aggsig_sender_receiver_interaction_offset() { &keychain.secp(), &rx_cx.sec_key, &rx_cx.sec_nonce, + None, &pub_nonce_sum, Some(&pub_key_sum), &msg, From d98e171847b73ae5f588172e0d28d0fe060f7bc9 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 21 Apr 2021 22:29:21 +0000 Subject: [PATCH 05/17] Add TxFlow enum to create blank Slate Add enum to select the transaction flow for a blank Slate --- api/src/foreign.rs | 6 +++--- api/src/owner.rs | 8 ++++---- api/src/owner_rpc.rs | 6 +++--- controller/src/command.rs | 8 ++++---- controller/tests/file.rs | 6 +++--- controller/tests/invoice.rs | 4 ++-- controller/tests/late_lock.rs | 4 ++-- controller/tests/no_change.rs | 4 ++-- controller/tests/payment_proofs.rs | 4 ++-- controller/tests/repost.rs | 6 +++--- controller/tests/slatepack.rs | 6 +++--- controller/tests/transaction.rs | 6 +++--- controller/tests/ttl_cutoff.rs | 6 +++--- libwallet/src/api_impl/owner.rs | 18 +++++++++++------ libwallet/src/internal/tx.rs | 6 +++--- libwallet/src/lib.rs | 2 +- libwallet/src/slate.rs | 28 ++++++++++++++++++++------ libwallet/src/slate_versions/v4_bin.rs | 4 ++-- libwallet/src/slate_versions/v5_bin.rs | 4 ++-- libwallet/src/slatepack/types.rs | 10 +++++---- 20 files changed, 85 insertions(+), 61 deletions(-) diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 813f35dc6..5196862ec 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -334,7 +334,7 @@ where /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); /// /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); - /// # let slate = Slate::blank(2, false); + /// # let slate = Slate::blank(2, TxFlow::Standard); /// /// // . . . /// // Obtain a sent slate somehow @@ -430,7 +430,7 @@ where /// // If result okay, send to payer, who will apply the transaction via their /// // owner API, then send back the slate /// // ... - /// # let slate = Slate::blank(2, true); + /// # let slate = Slate::blank(2, TxFlow::Invoice); /// /// let slate = api_foreign.finalize_tx(&slate, true); /// // if okay, then post via the owner API @@ -474,7 +474,7 @@ macro_rules! doctest_helper_setup_doc_env_foreign { use api::{Foreign, Owner}; use config::WalletConfig; use impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; - use libwallet::{BlockFees, IssueInvoiceTxArgs, Slate, WalletInst}; + use libwallet::{BlockFees, IssueInvoiceTxArgs, Slate, TxFlow, WalletInst}; // don't run on windows CI, which gives very inconsistent results if cfg!(windows) { diff --git a/api/src/owner.rs b/api/src/owner.rs index bf1710dd1..bbdbddaf9 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -27,8 +27,8 @@ use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, Status use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::{ AcctPathMapping, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, TxLogEntry, WalletInfo, - WalletInst, WalletLCProvider, + OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, TxFlow, TxLogEntry, + WalletInfo, WalletInst, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::SecretKey; @@ -801,7 +801,7 @@ where /// /// // . . . /// // The slate has been recieved from the invoicer, somehow - /// # let slate = Slate::blank(2, true); + /// # let slate = Slate::blank(2, TxFlow::Invoice); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: slate.amount, @@ -2338,7 +2338,7 @@ pub fn try_slatepack_sync_workflow( return Ok(None); } } - let mut ret_slate = Slate::blank(2, false); + let mut ret_slate = Slate::blank(2, TxFlow::Standard); let mut send_sync = |mut sender: HttpSlateSender, method_str: &str| match sender .send_tx(&slate, send_to_finalize) { diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 1b0f94939..21e09ab85 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -20,8 +20,8 @@ use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::{ AcctPathMapping, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, StatusMessage, - TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, + OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, + StatusMessage, TxFlow, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::{PublicKey, SecretKey}; @@ -2194,7 +2194,7 @@ pub fn run_doctest_owner( mask1.clone(), ); - let mut slate_outer = Slate::blank(2, false); + let mut slate_outer = Slate::blank(2, TxFlow::Standard); let rec_phrase_2 = util::ZeroingString::from( "hour kingdom ripple lunch razor inquiry coyote clay stamp mean \ diff --git a/controller/src/command.rs b/controller/src/command.rs index 722e344e7..6e8b5bd50 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -24,7 +24,7 @@ use crate::impls::SlateGetter as _; use crate::keychain; use crate::libwallet::{ self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack, - SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider, + SlatepackAddress, Slatepacker, SlatepackerArgs, TxFlow, WalletLCProvider, }; use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; @@ -274,7 +274,7 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { if args.estimate_selection_strategies { let strategies = vec!["smallest", "all"] @@ -495,7 +495,7 @@ where Some(s) => s, None => { // try and parse directly from input_slatepack_message - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); match message { Some(message) => { controller::owner_single_use( @@ -785,7 +785,7 @@ where { let issue_args = args.issue_args.clone(); - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { slate = api.issue_invoice_tx(m, issue_args)?; Ok(()) diff --git a/controller/tests/file.rs b/controller/tests/file.rs index 155ffd523..bc71306d9 100644 --- a/controller/tests/file.rs +++ b/controller/tests/file.rs @@ -26,7 +26,7 @@ use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; -use grin_wallet_libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate}; +use grin_wallet_libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, TxFlow}; #[macro_use] mod common; @@ -195,7 +195,7 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), ), }; - let mut slate = Slate::blank(2, true); + let mut slate = Slate::blank(2, TxFlow::Invoice); wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { let args = IssueInvoiceTxArgs { @@ -249,7 +249,7 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), format!("{}/standard_pp_S3.txbin", test_dir), ), }; - let mut slate = Slate::blank(2, true); + let mut slate = Slate::blank(2, TxFlow::Invoice); let mut address = None; wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { address = Some(api.get_slatepack_address(m, 0)?); diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index 0972ad5ae..307eda21b 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -21,7 +21,7 @@ use grin_wallet_libwallet as libwallet; use grin_wallet_util::grin_core as core; use impls::test_framework::{self, LocalWalletClient}; -use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, SlateState}; +use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, SlateState, TxFlow}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -95,7 +95,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { Ok(()) })?; - let mut slate = Slate::blank(2, true); + let mut slate = Slate::blank(2, TxFlow::Invoice); wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { // Wallet 2 inititates an invoice transaction, requesting payment diff --git a/controller/tests/late_lock.rs b/controller/tests/late_lock.rs index b038c6f88..f11d701e9 100644 --- a/controller/tests/late_lock.rs +++ b/controller/tests/late_lock.rs @@ -18,7 +18,7 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; -use self::libwallet::{InitTxArgs, Slate}; +use self::libwallet::{InitTxArgs, Slate, TxFlow}; use impls::test_framework::{self, LocalWalletClient}; use std::sync::atomic::Ordering; use std::thread; @@ -91,7 +91,7 @@ fn late_lock_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false)?; - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); let amount = 100_000_000_000; wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { diff --git a/controller/tests/no_change.rs b/controller/tests/no_change.rs index d171e195c..b12eddd95 100644 --- a/controller/tests/no_change.rs +++ b/controller/tests/no_change.rs @@ -21,7 +21,7 @@ use grin_wallet_util::grin_core as core; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; -use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate}; +use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, TxFlow}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -76,7 +76,7 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let fee = core::libtx::tx_fee(1, 1, 1); // send a single block's worth of transactions with minimal strategy - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); let mut stored_excess = None; wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { let args = InitTxArgs { diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs index 1176643db..31086edfc 100644 --- a/controller/tests/payment_proofs.rs +++ b/controller/tests/payment_proofs.rs @@ -20,7 +20,7 @@ extern crate grin_wallet_util; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; -use libwallet::{InitTxArgs, Slate}; +use libwallet::{InitTxArgs, Slate, TxFlow}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -84,7 +84,7 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err println!("Public address is: {:?}", address); let amount = 60_000_000_000; - let mut slate = Slate::blank(1, false); + let mut slate = Slate::blank(1, TxFlow::Standard); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index 41bd29603..a44d72110 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -20,7 +20,7 @@ extern crate grin_wallet_libwallet as libwallet; use grin_wallet_util::grin_core as core; -use self::libwallet::{InitTxArgs, Slate}; +use self::libwallet::{InitTxArgs, Slate, TxFlow}; use impls::test_framework::{self, LocalWalletClient}; use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; use std::sync::atomic::Ordering; @@ -99,7 +99,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let send_file = format!("{}/part_tx_1.tx", test_dir); let receive_file = format!("{}/part_tx_2.tx", test_dir); - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { @@ -198,7 +198,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> w.set_parent_key_id_by_name("account1")?; } - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); let amount = 60_000_000_000; wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { diff --git a/controller/tests/slatepack.rs b/controller/tests/slatepack.rs index 68168f22d..1e3116c03 100644 --- a/controller/tests/slatepack.rs +++ b/controller/tests/slatepack.rs @@ -28,7 +28,7 @@ use std::time::Duration; use grin_wallet_libwallet::{ InitTxArgs, IssueInvoiceTxArgs, Slate, Slatepack, SlatepackAddress, Slatepacker, - SlatepackerArgs, + SlatepackerArgs, TxFlow, }; use ed25519_dalek::PublicKey as edDalekPublicKey; @@ -300,7 +300,7 @@ fn slatepack_exchange_test_impl( ), }; - let mut slate = Slate::blank(2, true); + let mut slate = Slate::blank(2, TxFlow::Invoice); wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { let args = IssueInvoiceTxArgs { @@ -375,7 +375,7 @@ fn slatepack_exchange_test_impl( ), }; - let mut slate = Slate::blank(2, true); + let mut slate = Slate::blank(2, TxFlow::Invoice); let mut address = None; wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { address = Some(api.get_slatepack_address(m, 0)?); diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs index 1a66672dc..af8833301 100644 --- a/controller/tests/transaction.rs +++ b/controller/tests/transaction.rs @@ -22,7 +22,7 @@ use grin_wallet_util::grin_core as core; use self::core::core::transaction; use self::core::global; -use self::libwallet::{InitTxArgs, OutputStatus, Slate, SlateState}; +use self::libwallet::{InitTxArgs, OutputStatus, Slate, SlateState, TxFlow}; use impls::test_framework::{self, LocalWalletClient}; use std::convert::TryInto; use std::sync::atomic::Ordering; @@ -98,7 +98,7 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> // assert wallet contents // and a single use api for a send command let amount = 60_000_000_000; - let mut slate = Slate::blank(1, false); + let mut slate = Slate::blank(1, TxFlow::Standard); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { @@ -405,7 +405,7 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); let amount = 30_000_000_000; - let mut slate = Slate::blank(1, false); + let mut slate = Slate::blank(1, TxFlow::Standard); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { diff --git a/controller/tests/ttl_cutoff.rs b/controller/tests/ttl_cutoff.rs index 428caaa44..e4f4aaca7 100644 --- a/controller/tests/ttl_cutoff.rs +++ b/controller/tests/ttl_cutoff.rs @@ -20,7 +20,7 @@ extern crate grin_wallet_util; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; -use libwallet::{InitTxArgs, Slate, TxLogEntryType}; +use libwallet::{InitTxArgs, Slate, TxFlow, TxLogEntryType}; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -77,7 +77,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); let amount = 60_000_000_000; - let mut slate = Slate::blank(1, false); + let mut slate = Slate::blank(1, TxFlow::Standard); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { @@ -127,7 +127,7 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> })?; // try again, except try and send off the transaction for completion beyond the expiry - let mut slate = Slate::blank(1, false); + let mut slate = Slate::blank(1, TxFlow::Standard); wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 24d506a9e..94e805bd5 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -25,7 +25,7 @@ use crate::util::{OnionV3Address, OnionV3AddressError}; use crate::api_impl::owner_updater::StatusMessage; use crate::grin_keychain::{Identifier, Keychain}; use crate::internal::{keys, scan, selection, tx, updater}; -use crate::slate::{PaymentInfo, Slate, SlateState}; +use crate::slate::{PaymentInfo, Slate, SlateState, TxFlow}; use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; use crate::{ address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, @@ -471,7 +471,7 @@ where let mut slate = tx::new_tx_slate( &mut *w, args.amount, - false, + TxFlow::Standard, 2, use_test_rng, args.ttl_blocks, @@ -582,7 +582,7 @@ where None => w.parent_key_id(), }; - let mut slate = tx::new_tx_slate(&mut *w, args.amount, true, 2, use_test_rng, None)?; + let mut slate = tx::new_tx_slate(&mut *w, args.amount, TxFlow::Invoice, 2, use_test_rng, None)?; let height = w.w2n_client().get_chain_tip()?.0; let context = tx::add_output_to_slate( &mut *w, @@ -932,8 +932,14 @@ where // and insert into original context let current_height = w.w2n_client().get_chain_tip()?.0; - let mut temp_sl = - tx::new_tx_slate(&mut *w, context.amount, false, 2, false, args.ttl_blocks)?; + let mut temp_sl = tx::new_tx_slate( + &mut *w, + context.amount, + TxFlow::Standard, + 2, + false, + args.ttl_blocks, + )?; let temp_context = selection::build_send_tx( w, &keychain, @@ -1049,7 +1055,7 @@ where let tx_res = w.get_stored_tx(&format!("{}", id))?; match tx_res { Some(tx) => { - let mut slate = Slate::blank(2, false); + let mut slate = Slate::blank(2, TxFlow::Standard); slate.tx = Some(tx.clone()); slate.fee_fields = tx.aggregate_fee_fields().unwrap(); // apply fee mask past HF4 slate.id = id; diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 505b554db..9c932adf6 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -25,7 +25,7 @@ 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::slate::{Slate, TxFlow}; use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; use crate::util::OnionV3Address; use crate::InitTxArgs; @@ -47,7 +47,7 @@ lazy_static! { pub fn new_tx_slate<'a, T: ?Sized, C, K>( wallet: &mut T, amount: u64, - is_invoice: bool, + tx_flow: TxFlow, num_participants: u8, use_test_rng: bool, ttl_blocks: Option, @@ -58,7 +58,7 @@ where K: Keychain + 'a, { let current_height = wallet.w2n_client().get_chain_tip()?.0; - let mut slate = Slate::blank(num_participants, is_invoice); + let mut slate = Slate::blank(num_participants, tx_flow); if let Some(b) = ttl_blocks { slate.ttl_cutoff_height = current_height + b; } diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 2ae08f060..562ba5de8 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -56,7 +56,7 @@ pub mod slatepack; mod types; pub use crate::error::{Error, ErrorKind}; -pub use crate::slate::{ParticipantData, Slate, SlateState}; +pub use crate::slate::{ParticipantData, Slate, SlateState, TxFlow}; pub use crate::slate_versions::v4::sig_is_blank; pub use crate::slate_versions::{ SlateVersion, VersionedBinSlate, VersionedCoinbase, VersionedSlate, CURRENT_SLATE_VERSION, diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 57dddf8e7..5bc494f9b 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -205,6 +205,19 @@ impl fmt::Display for SlateState { } } +/// Transaction flow definition +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TxFlow { + /// Standard transaction flow, sender-receiver-sender + Standard, + /// Invoice transaction flow, receiver-sender-receiver + Invoice, + /// Multisig transaction flow + Multisig, + /// Atomic swap transaction flow + Atomic, +} + #[derive(Debug, Clone, PartialEq, Eq)] /// Kernel features arguments definition pub struct KernelFeaturesArgs { @@ -286,14 +299,16 @@ impl Slate { } /// Create a new slate - pub fn blank(num_participants: u8, is_invoice: bool) -> Slate { + pub fn blank(num_participants: u8, tx_flow: TxFlow) -> Slate { let np = match num_participants { 0 => 2, n => n, }; - let state = match is_invoice { - true => SlateState::Invoice1, - false => SlateState::Standard1, + let state = match tx_flow { + TxFlow::Standard => SlateState::Standard1, + TxFlow::Invoice => SlateState::Invoice1, + TxFlow::Multisig => SlateState::Multisig1, + TxFlow::Atomic => SlateState::Atomic1, }; Slate { num_participants: np, // assume 2 if not present @@ -314,6 +329,7 @@ impl Slate { kernel_features_args: None, } } + /// Removes any signature data that isn't mine, for compacting /// slates for a return journey pub fn remove_other_sigdata( @@ -1300,7 +1316,7 @@ pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { }; let secp = static_secp_instance(); let secp = secp.lock(); - let mut calc_slate = Slate::blank(2, false); + let mut calc_slate = Slate::blank(2, TxFlow::Standard); calc_slate.fee_fields = slate.fee; for d in slate.sigs.iter() { calc_slate.participant_data.push(ParticipantData { @@ -1371,7 +1387,7 @@ pub fn tx_from_slate_v5(slate: &SlateV5) -> Option { }; let secp = static_secp_instance(); let secp = secp.lock(); - let mut calc_slate = Slate::blank(2, false); + let mut calc_slate = Slate::blank(2, TxFlow::Standard); calc_slate.fee_fields = slate.fee; for d in slate.sigs.iter() { calc_slate.participant_data.push(ParticipantData { diff --git a/libwallet/src/slate_versions/v4_bin.rs b/libwallet/src/slate_versions/v4_bin.rs index fc0b1e998..afdcf1259 100644 --- a/libwallet/src/slate_versions/v4_bin.rs +++ b/libwallet/src/slate_versions/v4_bin.rs @@ -488,11 +488,11 @@ impl Readable for SlateV4Bin { fn slate_v4_serialize_deserialize() { use crate::grin_util::from_hex; use crate::grin_util::secp::key::PublicKey; - use crate::Slate; + use crate::{Slate, TxFlow}; use grin_wallet_util::grin_core::global::{set_local_chain_type, ChainTypes}; use grin_wallet_util::grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; set_local_chain_type(ChainTypes::Mainnet); - let slate = Slate::blank(1, false); + let slate = Slate::blank(1, TxFlow::Standard); let mut v4 = SlateV4::from(slate); let keychain = ExtKeychain::from_random_seed(true).unwrap(); diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs index 578322323..40efb35d8 100644 --- a/libwallet/src/slate_versions/v5_bin.rs +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -571,11 +571,11 @@ impl Readable for SlateV5Bin { fn slate_v5_serialize_deserialize() { use crate::grin_util::from_hex; use crate::grin_util::secp::key::PublicKey; - use crate::Slate; + use crate::{Slate, TxFlow}; use grin_wallet_util::grin_core::global::{set_local_chain_type, ChainTypes}; use grin_wallet_util::grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; set_local_chain_type(ChainTypes::Mainnet); - let slate = Slate::blank(1, false); + let slate = Slate::blank(1, TxFlow::Standard); let mut v5 = SlateV5::from(slate); let keychain = ExtKeychain::from_random_seed(true).unwrap(); diff --git a/libwallet/src/slatepack/types.rs b/libwallet/src/slatepack/types.rs index f56ecc9c1..60bc095a4 100644 --- a/libwallet/src/slatepack/types.rs +++ b/libwallet/src/slatepack/types.rs @@ -758,7 +758,7 @@ fn slatepack_bin_future() -> Result<(), grin_wallet_util::byte_ser::Error> { #[test] fn slatepack_encrypted_meta() -> Result<(), Error> { use crate::grin_core::global; - use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; + use crate::{Slate, SlateVersion, TxFlow, VersionedBinSlate, VersionedSlate}; use ed25519_dalek::PublicKey as edDalekPublicKey; use ed25519_dalek::SecretKey as edDalekSecretKey; use rand::{thread_rng, Rng}; @@ -780,7 +780,8 @@ fn slatepack_encrypted_meta() -> Result<(), Error> { slatepack.add_recipient(SlatepackAddress::random()); slatepack.add_recipient(SlatepackAddress::random()); - let v_slate = VersionedSlate::into_version(Slate::blank(2, false), SlateVersion::V4)?; + let v_slate = + VersionedSlate::into_version(Slate::blank(2, TxFlow::Standard), SlateVersion::V4)?; let bin_slate = VersionedBinSlate::try_from(v_slate).map_err(|_| ErrorKind::SlatepackSer)?; slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| ErrorKind::SlatepackSer)?; @@ -807,7 +808,7 @@ fn slatepack_encrypted_meta() -> Result<(), Error> { #[test] fn slatepack_encrypted_meta_future() -> Result<(), Error> { use crate::grin_core::global; - use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; + use crate::{Slate, SlateVersion, TxFlow, VersionedBinSlate, VersionedSlate}; use ed25519_dalek::PublicKey as edDalekPublicKey; use ed25519_dalek::SecretKey as edDalekSecretKey; use rand::{thread_rng, Rng}; @@ -829,7 +830,8 @@ fn slatepack_encrypted_meta_future() -> Result<(), Error> { slatepack.add_recipient(SlatepackAddress::random()); slatepack.add_recipient(SlatepackAddress::random()); - let v_slate = VersionedSlate::into_version(Slate::blank(2, false), SlateVersion::V4)?; + let v_slate = + VersionedSlate::into_version(Slate::blank(2, TxFlow::Standard), SlateVersion::V4)?; let bin_slate = VersionedBinSlate::try_from(v_slate).map_err(|_| ErrorKind::SlatepackSer)?; slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| ErrorKind::SlatepackSer)?; From 31f7613fd640010cf108b367434b1f567a2cbe73 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Fri, 4 Jun 2021 04:02:23 +0000 Subject: [PATCH 06/17] Build multisig transaction Add functionality for building a multisig transaction with a shared output --- api/src/owner.rs | 55 ++++++++ api/src/owner_rpc.rs | 28 ++++- controller/src/command.rs | 94 ++++++++++++++ impls/src/backends/lmdb.rs | 58 ++++++++- libwallet/src/api_impl/foreign.rs | 121 +++++++++++++++++- libwallet/src/api_impl/owner.rs | 28 ++++- libwallet/src/api_impl/types.rs | 4 + libwallet/src/internal/selection.rs | 186 ++++++++++++++++++++++++++-- libwallet/src/internal/tx.rs | 31 ++++- libwallet/src/slate.rs | 50 +++++++- libwallet/src/slate_versions/v5.rs | 1 + libwallet/src/types.rs | 80 ++++++++++++ src/bin/grin-wallet.yml | 20 +++ src/cmd/wallet_args.rs | 37 ++++++ util/src/lib.rs | 1 + 15 files changed, 765 insertions(+), 29 deletions(-) diff --git a/api/src/owner.rs b/api/src/owner.rs index bbdbddaf9..75f2f5f70 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -860,6 +860,61 @@ where } } + /// Process a multisig transaction to perform step 1 + 2 in the multisig bulletproof creation + /// process. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). The + /// receiver should have filled in round 0 and 1 of the multisig bulletproof. + /// + /// # Returns + /// * a result containing: + /// * The transaction [Slate](../grin_wallet_libwallet/slate/struct.Slate.html), + /// which can be forwarded to the receiving party by any means. Once the caller receives the final partial + /// bulletproof from the receiver, the associated wallet transaction outputs should be locked via a call to + /// [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs). This must be called before calling + /// [`finalize_tx`](struct.Owner.html#method.finalize_tx). + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * This method will store a partially completed transaction in the wallet's transaction log, + /// which will be updated on the corresponding call to [`finalize_tx`](struct.Owner.html#method.finalize_tx). + /// + /// # Example + /// Set up as in [new](struct.Owner.html#method.new) method above. + /// ```rust,no_run + /// # use grin_wallet_libwallet::TxFlow; + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// // Normally, slate is the output of multisig `receive_tx` + /// # let slate = Slate::blank(2, TxFlow::Multisig); + /// // Attempt to create a transaction using the 'default' account + /// let result = api_owner.process_multisig_tx( + /// None, + /// &slate, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// api_owner.tx_lock_outputs(None, &slate); + /// } + /// ``` + pub fn process_multisig_tx( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::process_multisig_tx(&mut **w, keychain_mask, slate) + } + /// Locks the outputs associated with the inputs to the transaction in the given /// [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html), /// making them unavailable for use in further transactions. This function is called diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 21e09ab85..f1d116616 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -469,7 +469,7 @@ pub trait OwnerRpc { ) -> Result; /** - ;Networked version of [Owner::process_invoice_tx](struct.Owner.html#method.process_invoice_tx). + Networked version of [Owner::process_invoice_tx](struct.Owner.html#method.process_invoice_tx). ``` # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( @@ -552,6 +552,17 @@ pub trait OwnerRpc { args: InitTxArgs, ) -> Result; + /** + Networked version of [Owner::process_multisig_tx](struct.Owner.html#method.process_multisig_tx). + + FIXME: add RPC doc-test + */ + fn process_multisig_tx( + &self, + token: Token, + slate: VersionedSlate, + ) -> Result; + /** Networked version of [Owner::tx_lock_outputs](struct.Owner.html#method.tx_lock_outputs). @@ -1825,6 +1836,21 @@ where Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) } + fn process_multisig_tx( + &self, + token: Token, + in_slate: VersionedSlate, + ) -> Result { + let out_slate = Owner::process_multisig_tx( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + ) + .map_err(|e| e.kind())?; + let version = SlateVersion::V5; + Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + } + fn finalize_tx( &self, token: Token, diff --git a/controller/src/command.rs b/controller/src/command.rs index 6e8b5bd50..c89597eb1 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -259,6 +259,7 @@ pub struct SendArgs { pub ttl_blocks: Option, pub skip_tor: bool, pub outfile: Option, + pub is_multisig: Option, } pub fn send( @@ -288,6 +289,7 @@ where num_change_outputs: args.change_outputs as u32, selection_strategy_is_use_all: strategy == "all", estimate_only: Some(true), + is_multisig: args.is_multisig, ..Default::default() }; let slate = api.init_send_tx(m, init_args)?; @@ -309,6 +311,7 @@ where ttl_blocks: args.ttl_blocks, send_args: None, late_lock: Some(args.late_lock), + is_multisig: args.is_multisig, ..Default::default() }; let result = api.init_send_tx(m, init_args); @@ -603,6 +606,85 @@ where } } +/// Process Multisig command arguments +#[derive(Clone)] +pub struct MultisigArgs { + pub input_file: Option, + pub input_slatepack_message: Option, + pub skip_tor: bool, + pub outfile: Option, +} + +pub fn process_multisig( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: MultisigArgs, + tor_config: Option, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (mut slate, ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let tor_config = match tor_config { + Some(mut c) => { + c.skip_send_attempt = Some(args.skip_tor); + Some(c) + } + None => None, + }; + + controller::owner_single_use( + Some(owner_api.wallet_inst.clone()), + keychain_mask, + Some(owner_api), + |api, m| { + slate = api.process_multisig_tx(m, &slate)?; + Ok(()) + }, + )?; + + let dest = match ret_address { + Some(a) => String::try_from(&a).unwrap(), + None => String::from(""), + }; + + let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + + match res { + Ok(Some(_)) => { + println!(); + println!( + "Transaction multisig bulletproof processed and sent back to recipient at {} for first round of finalization.", + dest + ); + println!(); + Ok(()) + } + Ok(None) => { + output_slatepack( + owner_api, + keychain_mask, + &slate, + &dest, + args.outfile, + false, + false, + )?; + Ok(()) + } + Err(e) => Err(e.into()), + } +} + pub fn unpack( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, @@ -750,6 +832,18 @@ where println!("Transaction finalized successfully"); + if slate + .participant_data + .iter() + .fold(false, |t, d| t | d.tau_x.is_some()) + { + info!( + "Transaction multisig identifier: {}", + slate.create_multisig_id().to_bip_32_string() + ); + info!("Use the identifier to select the multisig output in future transactions"); + } + output_slatepack( owner_api, keychain_mask, diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs index 6a8bd0343..a3b4bdc96 100644 --- a/impls/src/backends/lmdb.rs +++ b/impls/src/backends/lmdb.rs @@ -36,6 +36,7 @@ use crate::libwallet::{ }; use crate::util::secp::constants::SECRET_KEY_SIZE; use crate::util::secp::key::SecretKey; +use crate::util::secp::pedersen::Commitment; use crate::util::{self, secp, ToHex}; use rand::rngs::mock::StepRng; @@ -96,6 +97,24 @@ where Ok((ret_blind, ret_nonce)) } +/// XOR key to encrypt tau x key for multisig bulletproofs +fn tau_x_xor_key(keychain: &K, slate_id: &[u8]) -> Result<[u8; SECRET_KEY_SIZE], Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&b"tau_x"[..]); + let tau_x_xor_key = hasher.finalize(); + let mut ret_tau_x = [0; SECRET_KEY_SIZE]; + ret_tau_x.copy_from_slice(&tau_x_xor_key.as_bytes()[..SECRET_KEY_SIZE]); + + Ok(ret_tau_x) +} + pub struct LMDBBackend<'ck, C, K> where C: NodeClient + 'ck, @@ -273,6 +292,24 @@ where /*}*/ } + /// return the version of the commit for caching + fn calc_multisig_commit_for_cache( + &mut self, + keychain_mask: Option<&SecretKey>, + amount: u64, + id: &Identifier, + partial_commit: &Commitment, + ) -> Result<(Option, Option), Error> { + let keychain = self.keychain(keychain_mask)?; + let secp = keychain.secp(); + // TODO: proper support for different switch commitment schemes + let commit_key = keychain.derive_key(amount, id, SwitchCommitmentType::Regular)?; + let commit = secp.commit(0, commit_key)?; + let commit_sum = secp.commit_sum(vec![commit.clone(), partial_commit.clone()], vec![])?; + + Ok((Some(commit), Some(commit_sum))) + } + /// Set parent path by account name fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> { let label = label.to_owned(); @@ -334,16 +371,23 @@ where slate_id: &[u8], ) -> Result { let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); - let (blind_xor_key, nonce_xor_key) = - private_ctx_xor_keys(&self.keychain(keychain_mask)?, slate_id)?; + let k = self.keychain(keychain_mask)?; + let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(&k, slate_id)?; + let tau_x_xor_key = tau_x_xor_key(&k, slate_id)?; let mut ctx: Context = option_to_not_found(self.db.get_ser(&ctx_key), || { format!("Slate id: {:x?}", slate_id.to_vec()) })?; + let mut tau_none = [0; SECRET_KEY_SIZE]; + let tau_x_bytes = match ctx.tau_x.as_mut() { + Some(x) => &mut x.0, + None => &mut tau_none, + }; for i in 0..SECRET_KEY_SIZE { ctx.sec_key.0[i] ^= blind_xor_key[i]; ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; + tau_x_bytes[i] ^= tau_x_xor_key[i]; } Ok(ctx) @@ -691,12 +735,20 @@ where fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> { let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); - let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?; + let k = self.keychain(); + let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(k, slate_id)?; + let tau_x_xor_key = tau_x_xor_key(k, slate_id)?; let mut s_ctx = ctx.clone(); + let mut tau_none = [0; SECRET_KEY_SIZE]; + let tau_x_bytes = match s_ctx.tau_x.as_mut() { + Some(x) => &mut x.0, + None => &mut tau_none, + }; for i in 0..SECRET_KEY_SIZE { s_ctx.sec_key.0[i] ^= blind_xor_key[i]; s_ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; + tau_x_bytes[i] ^= tau_x_xor_key[i]; } self.db diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index d8cfdaac3..123066a5d 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -13,13 +13,17 @@ // limitations under the License. //! Generic implementation of owner API functions +use rand::thread_rng; use strum::IntoEnumIterator; use crate::api_impl::owner::finalize_tx as owner_finalize; use crate::api_impl::owner::{check_ttl, post_tx}; use crate::grin_core::core::FeeFields; -use crate::grin_keychain::Keychain; -use crate::grin_util::secp::key::SecretKey; +use crate::grin_core::libtx::proof; +use crate::grin_keychain::{Keychain, SwitchCommitmentType}; +use crate::grin_util::from_hex; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::pedersen::Commitment; use crate::internal::{selection, tx, updater}; use crate::slate_versions::SlateVersion; use crate::{ @@ -125,11 +129,17 @@ where ret_slate.amount = 0; ret_slate.fee_fields = FeeFields::zero(); ret_slate.remove_other_sigdata(&keychain, &context.sec_nonce, &context.sec_key)?; - ret_slate.state = SlateState::Standard2; + if ret_slate.is_multisig() { + ret_slate.state = SlateState::Multisig2; + } else { + ret_slate.state = SlateState::Standard2; + } Ok(ret_slate) } +/// + /// Receive a tx that this wallet has issued pub fn finalize_tx<'a, T: ?Sized, C, K>( w: &mut T, @@ -143,7 +153,7 @@ where K: Keychain + 'a, { let mut sl = slate.clone(); - let context = w.get_private_context(keychain_mask, sl.id.as_bytes())?; + let mut context = w.get_private_context(keychain_mask, sl.id.as_bytes())?; if sl.state == SlateState::Invoice2 { check_ttl(w, &sl)?; @@ -164,6 +174,109 @@ where } sl.state = SlateState::Invoice3; sl.amount = 0; + } else if sl.state == SlateState::Multisig3 { + let k = w.keychain(keychain_mask)?; + let secp = k.secp(); + let (_, pub_nonce) = context.get_public_keys(secp); + + let tau_one = context.tau_one.ok_or(Error::from(ErrorKind::GenericError( + "missing tau one multisig key".into(), + )))?; + let tau_two = context.tau_two.ok_or(Error::from(ErrorKind::GenericError( + "missing tau two multisig key".into(), + )))?; + + { + let oth_data = sl + .participant_data + .iter() + .find(|d| d.public_nonce != pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant's data".into(), + )))?; + let oth_tau_one = oth_data.tau_one.ok_or(Error::from(ErrorKind::GenericError( + "missing other tau one multisig key".into(), + )))?; + let oth_tau_two = oth_data.tau_two.ok_or(Error::from(ErrorKind::GenericError( + "missing other tau two multisig key".into(), + )))?; + + context.tau_one = Some(PublicKey::from_combination( + secp, + vec![&tau_one, &oth_tau_one], + )?); + context.tau_two = Some(PublicKey::from_combination( + secp, + vec![&tau_two, &oth_tau_two], + )?); + let common_nonce = context.create_common_nonce(secp, &oth_data.public_nonce)?; + let key_id = sl.create_multisig_id(); + let out = w.iter().find(|o| o.key_id == key_id).ok_or(Error::from( + ErrorKind::GenericError("missing multisig output".into()), + ))?; + + let commit_str = out + .commit + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output commit".into(), + )))?; + let commit_hex = from_hex(&commit_str) + .map_err(|e| ErrorKind::GenericError(format!("invalid hex: {}", e)))?; + let commit = Commitment::from_vec(commit_hex); + + // finish receiver's side of the multisig bulletproof + context.tau_x = Some(SecretKey::new(secp, &mut thread_rng())); + let _ = proof::create_multisig( + &k, + &proof::ProofBuilder::new(&k), + sl.amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + context.tau_x.as_mut(), + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit], + 2, + None, + )?; + } + + { + let mut part_data = sl + .participant_data + .iter_mut() + .find(|d| d.public_nonce == pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing local participant data".into(), + )))?; + part_data.tau_x = context.tau_x.clone(); + } + // FIXME: keep tau_one and tau_two in participant data to check on other end? + + let oth_tau_x = sl + .participant_data + .iter() + .find(|d| d.public_nonce != pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant's data".into(), + )))? + .tau_x + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing other tau x key".into(), + )))?; + + // compute tau x sum to store in the backend DB + let tau_x = context.tau_x.as_mut().unwrap(); + tau_x.add_assign(secp, oth_tau_x)?; + + sl.state = SlateState::Multisig4; + + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(sl.id.as_bytes().as_ref(), &context)?; + batch.commit()?; } else { sl = owner_finalize(w, keychain_mask, slate)?; } diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 94e805bd5..46d893b7b 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -14,19 +14,23 @@ //! Generic implementation of owner API functions +use rand::thread_rng; use uuid::Uuid; use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::Transaction; -use crate::grin_util::secp::key::SecretKey; -use crate::grin_util::Mutex; +use crate::grin_core::libtx::proof; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::{Mutex, ToHex}; use crate::util::{OnionV3Address, OnionV3AddressError}; use crate::api_impl::owner_updater::StatusMessage; -use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; use crate::slate::{PaymentInfo, Slate, SlateState, TxFlow}; -use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; +use crate::types::{ + AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletBackend, WalletInfo, +}; use crate::{ address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, @@ -500,6 +504,7 @@ where } let height = w.w2n_client().get_chain_tip()?.0; + let is_multisig = args.is_multisig.unwrap_or(false); let mut context = if args.late_lock.unwrap_or(false) { tx::create_late_lock_context( &mut *w, @@ -522,6 +527,7 @@ where args.selection_strategy_is_use_all, &parent_key_id, true, + is_multisig, use_test_rng, )? }; @@ -556,6 +562,10 @@ where slate.compact()?; + if slate.is_multisig() { + slate.state = SlateState::Multisig1; + } + Ok(slate) } @@ -675,6 +685,7 @@ where args.selection_strategy_is_use_all, &parent_key_id, false, + false, use_test_rng, )?; @@ -971,6 +982,15 @@ where tx_lock_outputs(w, keychain_mask, &sl)?; } + // finalize multisig bulletproof + if sl + .participant_data + .iter() + .fold(false, |t, d| t | d.tau_x.is_some()) + { + selection::finalize_multisig_bulletproof(&mut *w, keychain_mask, &mut sl, &mut context)?; + } + // Add our contribution to the offset sl.adjust_offset(&keychain, &context)?; diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 1a9fd238e..58b39c380 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -78,6 +78,9 @@ pub struct InitTxArgs { /// Sender arguments. If present, the underlying function will also attempt to send the /// transaction to a destination and optionally finalize the result pub send_args: Option, + /// If true, the transaction should contain a multisignature output shared by all the + /// participants + pub is_multisig: Option, } /// Send TX API Args, for convenience functionality that inits the transaction and sends @@ -109,6 +112,7 @@ impl Default for InitTxArgs { payment_proof_recipient_address: None, late_lock: Some(false), send_args: None, + is_multisig: None, } } } diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index c33879e7e..7e44164db 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -16,15 +16,16 @@ use crate::address; use crate::error::{Error, ErrorKind}; -use crate::grin_core::core::amount_to_hr_string; +use crate::grin_core::core::{amount_to_hr_string, Output, OutputFeatures}; use crate::grin_core::libtx::{ build, - proof::{ProofBuild, ProofBuilder}, + proof::{create_multisig, ProofBuild, ProofBuilder}, tx_fee, }; -use crate::grin_keychain::{Identifier, Keychain}; -use crate::grin_util::secp::key::SecretKey; +use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen; +use crate::grin_util::{from_hex, ToHex}; use crate::internal::keys; use crate::slate::Slate; use crate::types::*; @@ -251,19 +252,22 @@ where C: NodeClient + 'a, K: Keychain + 'a, { + let is_multisig = slate + .participant_data + .iter() + .fold(false, |t, d| t | d.part_commit.is_some()); + // Create a potential output for this transaction - let key_id = keys::next_available_key(wallet, keychain_mask).unwrap(); + let key_id = match is_multisig { + true => slate.create_multisig_id(), + false => keys::next_available_key(wallet, keychain_mask).unwrap(), + }; let keychain = wallet.keychain(keychain_mask)?; let key_id_inner = key_id.clone(); let amount = slate.amount; let height = current_height; let slate_id = slate.id; - slate.add_transaction_elements( - &keychain, - &ProofBuilder::new(&keychain), - vec![build::output(amount, key_id.clone())], - )?; // Add blinding sum to our context let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, is_initiator); @@ -271,7 +275,71 @@ where context.add_output(&key_id, &None, amount); context.amount = amount; context.fee = slate.fee_fields.as_opt(); - let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?; + + let (commit, output) = if is_multisig { + let (_, public_nonce) = context.get_public_keys(keychain.secp()); + let data = if use_test_rng { + assert_eq!(slate.participant_data.len(), 1); + &slate.participant_data[0] + } else { + slate + .participant_data + .iter() + .find(|d| d.public_nonce != public_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant data".into(), + )))? + }; + + let oth_partial_commit = data.part_commit.ok_or(Error::from(ErrorKind::Commit( + "missing partial commit".into(), + )))?; + + // calculate the commit sum of the participants' partial commits + let (partial_commit, commit_sum) = wallet.calc_multisig_commit_for_cache( + keychain_mask, + amount, + &key_id_inner, + &oth_partial_commit, + )?; + + context.partial_commit = partial_commit; + context.tau_one = Some(PublicKey::new()); + context.tau_two = Some(PublicKey::new()); + + // create the common nonce: SecretKey(SHA3("multisig_common_nonce" || secNonce*pubNonce)) + let oth_public_nonce = &data.public_nonce; + let common_nonce = context.create_common_nonce(keychain.secp(), oth_public_nonce)?; + + // calculate receiver's tau_one and tau_two public keys for the multisig bulletproof + let _ = create_multisig( + &keychain, + &ProofBuilder::new(&keychain), + amount, + &key_id_inner, + SwitchCommitmentType::Regular, + &common_nonce, + None, + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit_sum.clone().unwrap()], + 1, + None, + )?; + + ( + Some(commit_sum.unwrap().0.to_vec().to_hex()), + build::multisig_output(amount, key_id.clone(), oth_partial_commit.clone()), + ) + } else { + ( + wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?, + build::output(amount, key_id.clone()), + ) + }; + + slate.add_transaction_elements(&keychain, &ProofBuilder::new(&keychain), vec![output])?; + let mut batch = wallet.batch(keychain_mask)?; let log_id = batch.next_tx_log_id(&parent_key_id)?; let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id); @@ -287,8 +355,12 @@ where t.kernel_excess = Some(e) } t.kernel_lookup_min_height = Some(current_height); + let root_key_id = match is_multisig { + true => key_id.clone(), + false => parent_key_id.clone(), + }; batch.save(OutputData { - root_key_id: parent_key_id.clone(), + root_key_id: root_key_id, key_id: key_id_inner.clone(), mmr_index: None, n_child: key_id_inner.to_path().last_path_index(), @@ -696,3 +768,93 @@ where slate.tx_or_err_mut()?.offset = slate.offset.clone(); Ok(()) } + +pub fn finalize_multisig_bulletproof<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &mut Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = wallet.keychain(keychain_mask)?; + let secp = keychain.secp(); + let (_, pub_nonce) = context.get_public_keys(secp); + let oth_data = slate + .participant_data + .iter() + .find(|d| d.public_nonce != pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant data".into(), + )))?; + + let mut tau_x_sum = context + .tau_x + .clone() + .ok_or(Error::from(ErrorKind::GenericError( + "missing local tau x".into(), + )))?; + let oth_tau_x = oth_data + .tau_x + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing other tau x".into(), + )))?; + tau_x_sum.add_assign(secp, oth_tau_x)?; + + let common_nonce = context.create_common_nonce(secp, &oth_data.public_nonce)?; + + let key_id = slate.create_multisig_id(); + + let amount = slate.amount; + let out = wallet + .iter() + .find(|o| o.key_id == key_id) + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output".into(), + )))?; + let commit_str = out + .commit + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output commit".into(), + )))?; + let commit_hex = from_hex(&commit_str) + .map_err(|e| ErrorKind::GenericError(format!("invalid hex: {}", e)))?; + let commit = pedersen::Commitment::from_vec(commit_hex); + + let proof = create_multisig( + &keychain, + &ProofBuilder::new(&keychain), + amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + Some(&mut tau_x_sum), + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit.clone()], + 0, + None, + )? + .ok_or(Error::from(ErrorKind::GenericError( + "error creating final multisig proof".into(), + )))?; + + // replace the multisig output's rangeproof with the finalized multisig proof + let mut new_outs = vec![Output::new(OutputFeatures::Multisig, commit.clone(), proof)]; + let old_outs: Vec = slate + .tx_or_err()? + .outputs() + .iter() + .filter(|o| o.identifier.commit != commit) + .map(|o| o.clone()) + .collect(); + new_outs.extend_from_slice(&old_outs[..]); + slate.tx_or_err_mut()?.body.outputs = new_outs; + + Ok(()) +} diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 9c932adf6..7942b742d 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -20,7 +20,7 @@ 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_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; use crate::grin_util::Mutex; @@ -150,6 +150,7 @@ pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( selection_strategy_is_use_all: bool, parent_key_id: &Identifier, is_initiator: bool, + is_multisig: bool, use_test_rng: bool, ) -> Result where @@ -183,6 +184,16 @@ where is_initiator, )?; + if is_multisig { + // calculate partial commit to the amount + // used with receiver partial commit to calculate tau_one and tau_two in + // multisig bulletproof step 1 + let k = wallet.keychain(keychain_mask)?; + let key_id = slate.create_multisig_id(); + let partial_commit = k.commit(slate.amount, &key_id, SwitchCommitmentType::Regular)?; + context.partial_commit = Some(partial_commit); + } + // Generate a kernel offset and subtract from our context's secret key. Store // the offset in the slate's transaction kernel, and adds our public key // information to the slate @@ -234,12 +245,20 @@ where context.initial_sec_key = context.sec_key.clone(); + let is_multisig = slate + .participant_data + .iter() + .fold(false, |t, d| t | d.part_commit.is_some()); + if !is_initiator { // perform partial sig slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?; // update excess in stored transaction let mut batch = wallet.batch(keychain_mask)?; tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + if is_multisig { + batch.save_private_context(slate.id.as_bytes().as_ref(), &context)?; + } batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; batch.commit()?; } @@ -287,6 +306,16 @@ where context.amount = slate.amount; context.late_lock_args = Some(init_tx_args.clone()); + if init_tx_args.is_multisig.unwrap_or(false) { + // calculate partial commit to the amount + // used with receiver partial commit to calculate tau_one and tau_two in + // multisig bulletproof step 1 + let k = wallet.keychain(keychain_mask)?; + let key_id = slate.create_multisig_id(); + let partial_commit = k.commit(slate.amount, &key_id, SwitchCommitmentType::Regular)?; + context.partial_commit = Some(partial_commit); + } + // Generate a blinding factor for the tx and add // public key info to the slate slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?; diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 5bc494f9b..ddbfe53d7 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -15,6 +15,7 @@ //! Functions for building partial transactions to be passed //! around during an interactive wallet exchange +use crate::blake2::blake2b::Blake2b; use crate::error::{Error, ErrorKind}; use crate::grin_core::core::amount_to_hr_string; use crate::grin_core::core::transaction::{ @@ -23,7 +24,9 @@ use crate::grin_core::core::transaction::{ }; use crate::grin_core::libtx::{aggsig, build, proof::ProofBuild, tx_fee}; use crate::grin_core::map_vec; -use crate::grin_keychain::{BlindSum, BlindingFactor, Keychain, SwitchCommitmentType}; +use crate::grin_keychain::{ + BlindSum, BlindingFactor, ExtKeychainPath, Identifier, Keychain, SwitchCommitmentType, +}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; use crate::grin_util::secp::Signature; @@ -290,6 +293,16 @@ impl Slate { Ok(()) } + /// Get if the slate is for a multisig transaction + pub fn is_multisig(&self) -> bool { + self.participant_data.iter().fold(false, |t, d| { + t | d.tau_x.is_some() + | d.tau_one.is_some() + | d.tau_two.is_some() + | d.part_commit.is_some() + }) + } + /// Build a new empty transaction. /// Wallet currently only supports tx with "features and commit" inputs. pub fn empty_transaction() -> Transaction { @@ -544,6 +557,9 @@ impl Slate { let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; let mut part_sig = part_sig; let part_commit = context.partial_commit.clone(); + let tau_x = context.tau_x.clone(); + let tau_one = context.tau_one.clone(); + let tau_two = context.tau_two.clone(); // Remove if already here and replace self.participant_data = self @@ -567,9 +583,9 @@ impl Slate { public_atomic: None, part_sig: part_sig, part_commit, - tau_x: None, - tau_one: None, - tau_two: None, + tau_x: tau_x, + tau_one: tau_one, + tau_two: tau_two, }); Ok(()) } @@ -750,6 +766,32 @@ impl Slate { 4 => SlateVersion::V4, 5 | _ => SlateVersion::V5, } + } + + /// Calculate multisig key ID + pub fn create_multisig_id(&self) -> Identifier { + let mut hasher = Blake2b::new(16); + hasher.update(b"multisig_id"); + hasher.update(self.id.as_bytes().as_ref()); + hasher.update(self.amount.to_be_bytes().as_ref()); + let hash = hasher.finalize(); + let hash_bytes = hash.as_bytes(); + + let mut id_bytes = [[0; 4]; 4]; + id_bytes[0].copy_from_slice(&hash_bytes[..4]); + id_bytes[1].copy_from_slice(&hash_bytes[4..8]); + id_bytes[2].copy_from_slice(&hash_bytes[8..12]); + id_bytes[3].copy_from_slice(&hash_bytes[12..16]); + + let key_path = ExtKeychainPath::new( + 4, + u32::from_be_bytes(id_bytes[0]), + u32::from_be_bytes(id_bytes[1]), + u32::from_be_bytes(id_bytes[2]), + u32::from_be_bytes(id_bytes[3]), + ); + + Identifier::from_path(&key_path) } } diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs index 756764a41..63e4b2678 100644 --- a/libwallet/src/slate_versions/v5.rs +++ b/libwallet/src/slate_versions/v5.rs @@ -206,6 +206,7 @@ pub struct ParticipantDataV5 { /// Tau X key for shared outputs #[serde(default = "default_tau_x_none")] #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_seckey_serde")] pub tau_x: Option, /// Tau part one key for shared outputs #[serde(default = "default_tau_part_none")] diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 4929e561c..577d06d63 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -28,6 +28,7 @@ use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::{self, pedersen, Secp256k1}; use crate::grin_util::{ToHex, ZeroingString}; use crate::slate_versions::ser as dalek_ser; +use crate::util::sha3::{Digest, Sha3_256}; use crate::InitTxArgs; use chrono::prelude::*; use ed25519_dalek::PublicKey as DalekPublicKey; @@ -173,6 +174,15 @@ where id: &Identifier, ) -> Result, Error>; + /// return the multisig commit sum for caching if allowed, none otherwise + fn calc_multisig_commit_for_cache( + &mut self, + keychain_mask: Option<&SecretKey>, + amount: u64, + id: &Identifier, + partial_commit: &pedersen::Commitment, + ) -> Result<(Option, Option), Error>; + /// Set parent key id by stored account name fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error>; @@ -577,6 +587,12 @@ pub struct Context { pub calculated_excess: Option, /// For multisig only, store the partial commitment to the output value pub partial_commit: Option, + /// For multisig only, store the tau_one public key + pub tau_one: Option, + /// For multisig only, store the tau_two public key + pub tau_two: Option, + /// For multisig only, store the tau_x secret key + pub tau_x: Option, } impl Context { @@ -628,6 +644,9 @@ impl Context { late_lock_args: None, calculated_excess: None, partial_commit: None, + tau_one: None, + tau_two: None, + tau_x: None, } } } @@ -669,6 +688,25 @@ impl Context { ) } + /// Derive a common nonce using a Diffie-Hellman of the local secret nonce and + /// public nonce of the other participant + /// + /// The common nonce is: + /// + /// c = SecretKey(SHA3("multisig_common_nonce" || secNonce*pubNonce)) + pub fn create_common_nonce( + &self, + secp: &Secp256k1, + nonce: &PublicKey, + ) -> Result { + let mut common = nonce.clone(); + common.mul_assign(secp, &self.sec_key)?; + let mut hasher = Sha3_256::new(); + hasher.input(b"multisig_common_nonce"); + hasher.input(&common.serialize_vec(secp, true)); + SecretKey::from_slice(secp, &hasher.result()).map_err(|e| e.into()) + } + /// Create an atomic secret key pub fn create_atomic_secret(&mut self, secret: SecretKey) { self.sec_atomic = Some(secret); @@ -1138,4 +1176,46 @@ mod tests { let none2 = serde_json::from_str::("{}").unwrap(); assert_eq!(none, none2); } + + #[test] + fn context_tau_serde() { + let secp = Secp256k1::new(); + let mut ctx = Context::new( + &secp, + &Identifier::zero(), + true, /*use_test_rng*/ + false, /*is_initiator*/ + ); + + let sec_key = SecretKey::new(&secp, &mut thread_rng()); + ctx.tau_one = Some(PublicKey::from_secret_key(&secp, &sec_key).unwrap()); + ctx.tau_two = Some(PublicKey::from_secret_key(&secp, &sec_key).unwrap()); + ctx.tau_x = Some(sec_key); + + let val = serde_json::to_value(&ctx).unwrap(); + let des_ctx: Context = serde_json::from_value(val).unwrap(); + + assert!(des_ctx.tau_x.is_some()); + assert!(des_ctx.tau_one.is_some()); + assert!(des_ctx.tau_two.is_some()); + + ctx.tau_x = None; + + let val = serde_json::to_value(&ctx).unwrap(); + let des_ctx: Context = serde_json::from_value(val).unwrap(); + + assert!(des_ctx.tau_x.is_none()); + assert!(des_ctx.tau_one.is_some()); + assert!(des_ctx.tau_two.is_some()); + + ctx.tau_one = None; + ctx.tau_two = None; + + let val = serde_json::to_value(&ctx).unwrap(); + let des_ctx: Context = serde_json::from_value(val).unwrap(); + + assert!(des_ctx.tau_x.is_none()); + assert!(des_ctx.tau_one.is_none()); + assert!(des_ctx.tau_two.is_none()); + } } diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 59b227b7b..19fff32a6 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -111,6 +111,9 @@ subcommands: help: EXPERIMENTAL - Do not lock the coins immediately, instead only lock them during finalization. short: l long: late-lock + - mutlisig: + help: EXPERIMENTAL - Create a multisig output with shared ownership. + long: multisig - change_outputs: help: Number of change outputs to generate (mainly for testing) short: o @@ -174,6 +177,23 @@ subcommands: short: u long: outfile takes_value: true + - process_multisig: + about: Processes a Slatepack Message to perform step 1 + 2 in multisig bulletproof building + args: + - input: + help: File containing a Slatepack Message + short: i + long: input + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true - finalize: about: Processes a Slatepack Message to finalize a transfer. args: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 70d542cd6..8b5f6d2eb 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -495,6 +495,8 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Result Result { + // input file + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + let outfile = parse_optional(args, "outfile")?; + + Ok(command::MultisigArgs { + input_file, + input_slatepack_message, + skip_tor: args.is_present("manual"), + outfile, + }) +} + pub fn parse_info_args(args: &ArgMatches) -> Result { // minimum_confirmations let mc = parse_required(args, "minimum_confirmations")?; @@ -1164,6 +1197,10 @@ where test_mode, ) } + ("process_multisig", Some(args)) => { + let a = arg_parse!(parse_process_multisig_args(&args)); + command::process_multisig(owner_api, km, a, Some(tor_config.clone()), test_mode) + } ("info", Some(args)) => { let a = arg_parse!(parse_info_args(&args)); command::info( diff --git a/util/src/lib.rs b/util/src/lib.rs index 8b8bd8dec..2482d2a3a 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -26,6 +26,7 @@ extern crate serde_derive; mod ov3; pub use ov3::OnionV3Address; pub use ov3::OnionV3Error as OnionV3AddressError; +pub use sha3; #[allow(missing_docs)] pub mod byte_ser; From e6dbdb7fd6248628fc5a1a4c90bfb7c7e206cb32 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Fri, 7 May 2021 22:23:36 +0000 Subject: [PATCH 07/17] Add functions for storing atomic secret Add functions for getting and storing atomic secret in the wallet backend --- impls/src/backends/lmdb.rs | 57 ++++++++++++++++++++++++++++++++++++++ libwallet/src/types.rs | 14 ++++++++++ 2 files changed, 71 insertions(+) diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs index a3b4bdc96..4dd0dbd6b 100644 --- a/impls/src/backends/lmdb.rs +++ b/impls/src/backends/lmdb.rs @@ -56,6 +56,7 @@ const LAST_SCANNED_BLOCK: u8 = b'l'; const LAST_SCANNED_KEY: &str = "LAST_SCANNED_KEY"; const WALLET_INIT_STATUS: u8 = b'w'; const WALLET_INIT_STATUS_KEY: &str = "WALLET_INIT_STATUS"; +const ATOMIC_SECRET_PREFIX: u8 = b's'; /// test to see if database files exist in the current directory. If so, /// use a DB backend for all operations @@ -115,6 +116,25 @@ where Ok(ret_tau_x) } +fn atomic_xor_key(keychain: &K, atomic_id: &Identifier) -> Result<[u8; SECRET_KEY_SIZE], Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + + //derive XOE value for storing atomic public key + // h(root_key|atomic_id|"atomic_secret") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0); + hasher.update(&atomic_id.to_bytes()); + hasher.update(&b"atomic_secret"[..]); + let atomic_xor_key = hasher.finalize(); + let mut ret_atomic = [0; SECRET_KEY_SIZE]; + ret_atomic.copy_from_slice(&atomic_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + Ok(ret_atomic) +} + pub struct LMDBBackend<'ck, C, K> where C: NodeClient + 'ck, @@ -532,6 +552,25 @@ where }; Ok(status) } + + fn get_atomic_secret<'a>( + &mut self, + keychain_mask: Option<&SecretKey>, + atomic_id: &Identifier, + ) -> Result { + let keychain = self.keychain(keychain_mask)?; + let mut xor_key = atomic_xor_key(&keychain, atomic_id)?; + let secret_key = to_key(ATOMIC_SECRET_PREFIX, &mut atomic_id.to_bytes().to_vec()); + let batch = self.db.batch()?; + let secret: Vec = match batch.get_ser(&secret_key)? { + Some(s) => s, + None => return Err(ErrorKind::GenericError("missing atomic secret".into()).into()), + }; + for (x, s) in xor_key.iter_mut().zip(secret.iter()) { + *x ^= s; + } + Ok(SecretKey::from_slice(keychain.secp(), xor_key.as_ref())?) + } } /// An atomic batch in which all changes can be committed all at once or @@ -774,4 +813,22 @@ where db.unwrap().commit()?; Ok(()) } + + fn save_atomic_secret( + &mut self, + atomic_id: &Identifier, + atomic_secret: &SecretKey, + ) -> Result<(), Error> { + let mut xor_key = atomic_xor_key(self.keychain(), atomic_id)?; + let secret_key = to_key(ATOMIC_SECRET_PREFIX, &mut atomic_id.to_bytes().to_vec()); + for (x, s) in xor_key.iter_mut().zip(atomic_secret.0[..].iter()) { + *x ^= s; + } + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&secret_key, &xor_key.to_vec())?; + Ok(()) + } } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 577d06d63..b76b61e42 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -247,6 +247,13 @@ where /// Flag whether the wallet needs a full UTXO scan on next update attempt fn init_status(&mut self) -> Result; + + /// Get the secret for an atomic swap transaction + fn get_atomic_secret( + &mut self, + keychain_mask: Option<&SecretKey>, + atomic_id: &Identifier, + ) -> Result; } /// Batch trait to update the output data backend atomically. Trying to use a @@ -315,6 +322,13 @@ where /// Write the wallet data to backend file fn commit(&self) -> Result<(), Error>; + + /// Save secret for an atomic swap transaction + fn save_atomic_secret( + &mut self, + atomic_id: &Identifier, + secret: &SecretKey, + ) -> Result<(), Error>; } /// Encapsulate all wallet-node communication functions. No functions within libwallet From 2f053de3bf791a5182ad95360229ae188fa53d4e Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Fri, 7 May 2021 22:43:42 +0000 Subject: [PATCH 08/17] Add functions to create an atomic identifier Add functions for creating and checking an atomic identifier Use for deriving atomic nonce's from a wallet's keychain --- libwallet/src/slate.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index ddbfe53d7..552a99f5a 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -25,7 +25,7 @@ use crate::grin_core::core::transaction::{ use crate::grin_core::libtx::{aggsig, build, proof::ProofBuild, tx_fee}; use crate::grin_core::map_vec; use crate::grin_keychain::{ - BlindSum, BlindingFactor, ExtKeychainPath, Identifier, Keychain, SwitchCommitmentType, + BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Identifier, Keychain, SwitchCommitmentType, }; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; @@ -50,6 +50,8 @@ use crate::slate_versions::{SlateVersion, VersionedSlate}; use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; use crate::Context; +pub const ATOMIC_ID_PREFIX: &'static [u8] = b"\x03mwatomic"; + #[derive(Debug, Clone)] pub struct PaymentInfo { /// Sender address @@ -399,6 +401,33 @@ impl Slate { Ok(()) } + /// Create an atomic secret identifier with the prefix b'\x04mwatomic' + pub fn create_atomic_id(id: u32) -> Identifier { + ExtKeychain::derive_key_id( + 3, 0x6d776174, /* 'mwat' */ + 0x6f6d6963, /* 'omic' */ + id, 0, + ) + } + + /// Check that an atomic secret identifier is valid + pub fn check_atomic_id(id: &Identifier) -> Result<(), Error> { + let id_bytes = id.to_bytes(); + if &id_bytes[..9] == ATOMIC_ID_PREFIX { + Ok(()) + } else { + Err(ErrorKind::GenericError("Invalid atomic ID".into()).into()) + } + } + + /// Convert the atomic secret identifier to an unsigned integer + pub fn atomic_id_to_int(id: &Identifier) -> Result { + Self::check_atomic_id(id)?; + let mut id_bytes = [0; 4]; + id_bytes.copy_from_slice(&id.to_bytes()[9..13]); + Ok(u32::from_be_bytes(id_bytes)) + } + /// Completes callers part of round 1, adding public key info /// to the slate pub fn fill_round_1(&mut self, keychain: &K, context: &mut Context) -> Result<(), Error> From 2dc9de379128639d695f2a92d20403bf9071fbaf Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Thu, 22 Apr 2021 02:45:02 +0000 Subject: [PATCH 09/17] Add functions for atomic swap transaction flow --- libwallet/src/slate.rs | 127 ++++++++++++++++++++++++++++- libwallet/src/types.rs | 15 ++-- libwallet/tests/libwallet.rs | 153 +++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 11 deletions(-) diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 552a99f5a..28383467c 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -401,6 +401,33 @@ impl Slate { Ok(()) } + /// Find the pariticipant data index associated with the given index + pub fn find_participant_data_index( + &self, + secp: &secp::Secp256k1, + context: &Context, + ) -> Result { + let (public_blind, public_nonce) = context.get_public_keys(secp); + if let Some((i, _p)) = + self.participant_data.iter().enumerate().find(|(_i, p)| { + p.public_blind_excess == public_blind && p.public_nonce == public_nonce + }) { + Ok(i) + } else { + Err(ErrorKind::StoredTx("Missing participant data".into()).into()) + } + } + + /// In a two-party transaction, find the participant data index other than the one provided + pub fn find_other_participant_data_index(&self, idx: usize) -> Result { + for i in 0..self.participant_data.len() { + if i != idx { + return Ok(i); + } + } + Err(ErrorKind::StoredTx("Missing participant data".into()).into()) + } + /// Create an atomic secret identifier with the prefix b'\x04mwatomic' pub fn create_atomic_id(id: u32) -> Identifier { ExtKeychain::derive_key_id( @@ -515,6 +542,103 @@ impl Slate { Ok(()) } + /// Create a partial signature over the Slate using the atomic swap receiver's + /// secret atomic nonce + pub fn fill_round_2_atomic(&mut self, keychain: &K, context: &Context) -> Result<(), Error> + where + K: Keychain, + { + let secp = keychain.secp(); + let part_sig = aggsig::calculate_partial_sig( + secp, + &context.sec_key, + &context.sec_nonce, + context.get_secret_atomic(), + &self.pub_nonce_sum(secp)?, + Some(&self.pub_blind_sum(secp)?), + &self.msg_to_sign()?, + )?; + + let pdata_idx = self.find_participant_data_index(secp, context)?; + self.participant_data[pdata_idx].part_sig = Some(part_sig); + Ok(()) + } + + /// Verify the receiver's adaptor signature, and create the sender's partial signature + pub fn fill_round_3_atomic( + &mut self, + keychain: &K, + context: &Context, + ) -> Result + where + K: Keychain, + { + let secp = keychain.secp(); + let pdata_idx = self.find_participant_data_index(secp, context)?; + let opdata_idx = self.find_other_participant_data_index(pdata_idx)?; + let part_sig = &self.participant_data[opdata_idx].part_sig.ok_or::( + ErrorKind::Signature("Missing round 2 atomic swap adaptor signature".into()).into(), + )?; + let msg = self.msg_to_sign()?; + let nonce_sum = self.pub_nonce_sum(secp)?; + let key_sum = self.pub_blind_sum(secp)?; + + aggsig::verify_partial_sig( + secp, + part_sig, + &nonce_sum, + self.participant_data[opdata_idx].public_atomic.as_ref(), + &self.participant_data[opdata_idx].public_blind_excess, + Some(&key_sum), + &msg, + )?; + + let a_part_sig = aggsig::calculate_partial_sig( + secp, + &context.sec_key, + &context.sec_nonce, + None, + &nonce_sum, + Some(&key_sum), + &msg, + )?; + + self.participant_data[pdata_idx].part_sig = Some(a_part_sig); + + // return the receiver's adaptor signature from round 2 + Ok((*part_sig).clone()) + } + + /// Finalize the atomic swap transaction, return the receiver's partial signature + pub fn finalize_atomic( + &mut self, + keychain: &K, + context: &Context, + ) -> Result + where + K: Keychain, + { + let secp = keychain.secp(); + + let part_sig = aggsig::calculate_partial_sig( + secp, + &context.sec_key, + &context.sec_nonce, + None, + &self.pub_nonce_sum(secp)?, + Some(&self.pub_blind_sum(secp)?), + &self.msg_to_sign()?, + )?; + + let pdata_idx = self.find_participant_data_index(secp, context)?; + self.participant_data[pdata_idx].part_sig = Some(part_sig.clone()); + + let final_sig = self.finalize_signature(secp)?; + self.finalize_transaction(keychain, &final_sig)?; + + Ok(part_sig) + } + /// Creates the final signature, callable by either the sender or recipient /// (after phase 3: sender confirmation) pub fn finalize(&mut self, keychain: &K) -> Result<(), Error> @@ -589,6 +713,7 @@ impl Slate { let tau_x = context.tau_x.clone(); let tau_one = context.tau_one.clone(); let tau_two = context.tau_two.clone(); + let pub_atomic = context.get_public_atomic(keychain.secp())?; // Remove if already here and replace self.participant_data = self @@ -609,7 +734,7 @@ impl Slate { self.participant_data.push(ParticipantData { public_blind_excess: pub_key, public_nonce: pub_nonce, - public_atomic: None, + public_atomic: pub_atomic, part_sig: part_sig, part_commit, tau_x: tau_x, diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index b76b61e42..5b667b807 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -721,20 +721,15 @@ impl Context { SecretKey::from_slice(secp, &hasher.result()).map_err(|e| e.into()) } - /// Create an atomic secret key - pub fn create_atomic_secret(&mut self, secret: SecretKey) { + /// Set an atomic secret + pub fn set_secret_atomic(&mut self, secret: SecretKey) { self.sec_atomic = Some(secret); } - /// Set an atomic secret key - pub fn set_atomic_secret(&mut self, secret: SecretKey) { - self.sec_atomic = Some(secret); - } - - /// Get the atomic secret key - pub fn get_secret_atomic_secret(&self) -> Option<&SecretKey> { + /// Get the atomic secret + pub fn get_secret_atomic(&self) -> Option<&SecretKey> { match &self.sec_atomic { - Some(n) => Some(n), + Some(a) => Some(a), None => None, } } diff --git a/libwallet/tests/libwallet.rs b/libwallet/tests/libwallet.rs index bf7ce3d22..b51d26b3e 100644 --- a/libwallet/tests/libwallet.rs +++ b/libwallet/tests/libwallet.rs @@ -535,3 +535,156 @@ fn test_rewind_range_proof() { .unwrap(); assert!(proof_info.is_none()); } + +#[test] +fn test_atomic_swap_multisig_tx() { + use grin_wallet_libwallet::{Slate, TxFlow}; + use grin_wallet_util::grin_core::global; + + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let mut slate = Slate::blank(2, TxFlow::Atomic); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let switch = SwitchCommitmentType::Regular; + + let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let sender_blind = sender_keychain.derive_key(0, &key_id, switch).unwrap(); + let receiver_blind = receiver_keychain.derive_key(0, &key_id, switch).unwrap(); + let in_commit = sender_keychain.commit(23000015, &key_id, switch).unwrap(); + let change_commit = sender_keychain.commit(10, &key_id, switch).unwrap(); + let out_commit = receiver_keychain.commit(5, &key_id, switch).unwrap(); + + let input = transaction::Input::new(transaction::OutputFeatures::Plain, in_commit); + + // create the change output range proof + let change_builder = proof::ProofBuilder::new(&receiver_keychain); + let change_proof = proof::create( + &sender_keychain, + &change_builder, + 10, + &key_id, + switch, + change_commit, + None, + ) + .unwrap(); + + let change = transaction::Output::new( + transaction::OutputFeatures::Plain, + change_commit, + change_proof, + ); + + // create the output range proof + let out_builder = proof::ProofBuilder::new(&receiver_keychain); + let out_proof = proof::create( + &receiver_keychain, + &out_builder, + 5, + &key_id, + switch, + out_commit, + None, + ) + .unwrap(); + + let output = + transaction::Output::new(transaction::OutputFeatures::Plain, out_commit, out_proof); + + // set the fee to the minimum for a single (input, output, kernel) tx + let kernel = transaction::TxKernel { + features: transaction::KernelFeatures::Plain { + fee: 23000000.into(), + }, + excess: in_commit, + excess_sig: secp::Signature::from_raw_data(&[0; 64]).unwrap(), + }; + + let mut sender_ctx; + { + // dealing with an input here so we need to negate the blinding_factor + // rather than use it as is + let bs = BlindSum::new(); + let blinding_factor = sender_keychain + .blind_sum(&bs.sub_blinding_factor(BlindingFactor::from_secret_key(sender_blind))) + .unwrap(); + + let blind = blinding_factor.secret_key(sender_keychain.secp()).unwrap(); + + sender_ctx = Context::with_excess(sender_keychain.secp(), blind, &key_id, false); + } + let mut receiver_ctx = + Context::with_excess(receiver_keychain.secp(), receiver_blind, &key_id, false); + + // set the atomic nonce used for the adaptor signature + let atomic_secret = SecretKey::from_slice(receiver_keychain.secp(), &[2; 32]).unwrap(); + receiver_ctx.set_atomic_secret(atomic_secret.clone()); + + // set kernel features, fee, and total amount to sign the correct kernel message + slate.kernel_features = 0; + slate.fee_fields = transaction::FeeFields::new(0, 23000000).unwrap(); + slate.amount = 23000015; + + slate + .fill_round_1(&sender_keychain, &mut sender_ctx) + .unwrap(); + + // adjust kernel offset to match correct kernel sum + sender_ctx.input_ids = vec![(key_id.clone(), None, 23000015)]; + sender_ctx.output_ids = vec![(key_id.clone(), None, 10)]; + slate.adjust_offset(&sender_keychain, &sender_ctx).unwrap(); + + slate + .fill_round_1(&receiver_keychain, &mut receiver_ctx) + .unwrap(); + + // adjust kernel offset to match correct kernel sum + receiver_ctx.output_ids = vec![(key_id.clone(), None, 5)]; + slate + .adjust_offset(&receiver_keychain, &receiver_ctx) + .unwrap(); + + slate + .fill_round_2_atomic(&receiver_keychain, &mut receiver_ctx) + .unwrap(); + + let adaptor_sig = slate + .fill_round_3_atomic(&sender_keychain, &mut sender_ctx) + .unwrap(); + + let mut tx = transaction::Transaction::empty() + .with_input(input) + .with_output(change) + .with_output(output) + .with_kernel(kernel); + tx.offset = slate.offset.clone(); + slate.tx = Some(tx); + + let rec_part_sig = slate + .finalize_atomic(&receiver_keychain, &mut receiver_ctx) + .unwrap(); + + // calculate sr' - sr, and validate it equals the receiver's atomic nonce + let secp = receiver_keychain.secp(); + let mut srp = SecretKey::from_slice(secp, &adaptor_sig.as_ref()[32..]).unwrap(); + let mut sr = SecretKey::from_slice(secp, &rec_part_sig.as_ref()[32..]).unwrap(); + sr.neg_assign(secp).unwrap(); + srp.add_assign(secp, &sr).unwrap(); + assert_eq!(srp, atomic_secret); + + // now, do the same thing, but with the final signature + let full_sig = slate.tx.unwrap().kernels()[0].excess_sig.clone(); + let send_part_sig = slate.participant_data[0].part_sig.unwrap(); + sr = SecretKey::from_slice(secp, &full_sig.as_ref()[32..]).unwrap(); + // subtract the sender's partial sig from the final sig + let mut sp_s = SecretKey::from_slice(secp, &send_part_sig.as_ref()[32..]).unwrap(); + sp_s.neg_assign(secp).unwrap(); + sr.add_assign(secp, &sp_s).unwrap(); + // now sr contains the receiver's partial sig + // calculate sr' - sr, and validate it equals the receiver's atomic nonce + sr.neg_assign(secp).unwrap(); + srp = SecretKey::from_slice(secp, &adaptor_sig.as_ref()[32..]).unwrap(); + srp.add_assign(secp, &sr).unwrap(); + assert_eq!(srp, atomic_secret); +} From ce82f5e996287fb36a64c5f80180f7a5160f0339 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Tue, 27 Apr 2021 22:05:37 +0000 Subject: [PATCH 10/17] Add owner API to init an atomic swap --- libwallet/src/api_impl/owner.rs | 129 ++++++++++++++++++++++++++++++-- libwallet/src/slate.rs | 2 +- libwallet/tests/libwallet.rs | 6 +- 3 files changed, 126 insertions(+), 11 deletions(-) diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 46d893b7b..7b6bdeba6 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -27,14 +27,12 @@ use crate::util::{OnionV3Address, OnionV3AddressError}; use crate::api_impl::owner_updater::StatusMessage; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; -use crate::slate::{PaymentInfo, Slate, SlateState, TxFlow}; -use crate::types::{ - AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletBackend, WalletInfo, -}; +use crate::slate::{KernelFeaturesArgs, PaymentInfo, Slate, SlateState, TxFlow}; +use crate::types::{AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletBackend, WalletInfo}; use crate::{ - address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, - PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, - TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, + address, wallet_lock, Context, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, + OutputCommitMapping, PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, + SlatepackerArgs, TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, }; use crate::{Error, ErrorKind}; use ed25519_dalek::PublicKey as DalekPublicKey; @@ -921,6 +919,123 @@ where ) } +/// Initialize sender atomic swap transaction +pub fn init_atomic_swap<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: InitTxArgs, + derive_path: u32, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = match &args.src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.clone())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + + let mut slate = tx::new_tx_slate( + &mut *w, + args.amount, + TxFlow::Atomic, + 2, + use_test_rng, + args.ttl_blocks, + )?; + + if let Some(v) = args.target_slate_version { + slate.version_info.version = v; + }; + + let height = w.w2n_client().get_chain_tip()?.0; + let keychain = w.keychain(keychain_mask)?; + // FIXME: need a way to ensure atomic secrets are not reused + // + // The attack vector is being supplied/tricked into reusing the + // same derivation path for multiple swaps, where the swap has + // already been completed successfully. + // + // The attacker would not need to wait for the full signature + // in the repeat swap, since they would have the nonce already. + // + // Maybe use a bloom filter as an automated defense? + let atomic_id = Slate::create_atomic_id(derive_path as u64); + slate.atomic_id = Some(atomic_id); + let mut context = if args.late_lock.unwrap_or(false) { + // use late_lock context for initial height_lock tx, + // initiated by the atomic swap receiver + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); + context.amount = args.amount; + context.fee = slate.fee_fields.as_opt(); + + slate.fill_round_1(&keychain, &mut context)?; + + // Create a height_lock kernel, with a lock_height set to an + // arbitrary amount of blocks in the future + // + // FIXME: add option to specify the lock_height? + slate.kernel_features = 2; + slate.kernel_features_args = Some(KernelFeaturesArgs { + lock_height: height + 60, + }); + + context + } else { + let mut context = tx::add_inputs_to_atomic_slate( + w, + keychain_mask, + &mut slate, + height, + args.minimum_confirmations, + args.max_outputs as usize, + args.num_change_outputs as usize, + args.selection_strategy_is_use_all, + &parent_key_id, + None, + use_test_rng, + )?; + slate.fill_round_1(&keychain, &mut context)?; + context + }; + + // Payment Proof, add addresses to slate and save address + if let Some(a) = args.payment_proof_recipient_address { + let sec_addr_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derive_path)?; + let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; + + slate.payment_proof = Some(PaymentInfo { + sender_address: sender_address.to_ed25519()?, + receiver_address: a.pub_key, + receiver_signature: None, + }); + + context.payment_proof_derivation_index = Some(derive_path); + } + + // Save the aggsig context in our DB for when we + // receive the transaction back + { + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + slate.adjust_offset(&keychain, &context)?; + slate.compact()?; + + Ok(slate) +} + /// Finalize slate pub fn finalize_tx<'a, T: ?Sized, C, K>( w: &mut T, diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 28383467c..b663d3fe9 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -543,7 +543,7 @@ impl Slate { } /// Create a partial signature over the Slate using the atomic swap receiver's - /// secret atomic nonce + /// atomic secret pub fn fill_round_2_atomic(&mut self, keychain: &K, context: &Context) -> Result<(), Error> where K: Keychain, diff --git a/libwallet/tests/libwallet.rs b/libwallet/tests/libwallet.rs index b51d26b3e..fd686da8f 100644 --- a/libwallet/tests/libwallet.rs +++ b/libwallet/tests/libwallet.rs @@ -617,7 +617,7 @@ fn test_atomic_swap_multisig_tx() { let mut receiver_ctx = Context::with_excess(receiver_keychain.secp(), receiver_blind, &key_id, false); - // set the atomic nonce used for the adaptor signature + // set the atomic secret used for the adaptor signature let atomic_secret = SecretKey::from_slice(receiver_keychain.secp(), &[2; 32]).unwrap(); receiver_ctx.set_atomic_secret(atomic_secret.clone()); @@ -665,7 +665,7 @@ fn test_atomic_swap_multisig_tx() { .finalize_atomic(&receiver_keychain, &mut receiver_ctx) .unwrap(); - // calculate sr' - sr, and validate it equals the receiver's atomic nonce + // calculate sr' - sr, and validate it equals the receiver's atomic secret let secp = receiver_keychain.secp(); let mut srp = SecretKey::from_slice(secp, &adaptor_sig.as_ref()[32..]).unwrap(); let mut sr = SecretKey::from_slice(secp, &rec_part_sig.as_ref()[32..]).unwrap(); @@ -682,7 +682,7 @@ fn test_atomic_swap_multisig_tx() { sp_s.neg_assign(secp).unwrap(); sr.add_assign(secp, &sp_s).unwrap(); // now sr contains the receiver's partial sig - // calculate sr' - sr, and validate it equals the receiver's atomic nonce + // calculate sr' - sr, and validate it equals the receiver's atomic secret sr.neg_assign(secp).unwrap(); srp = SecretKey::from_slice(secp, &adaptor_sig.as_ref()[32..]).unwrap(); srp.add_assign(secp, &sr).unwrap(); From 57da2ff78073cd5168001c3b01f95fc5ce6c08e0 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Tue, 27 Apr 2021 22:29:50 +0000 Subject: [PATCH 11/17] Add foreign API to receive an atomic swap tx Add foreign API to receive and complete the second round of an atomic swap transaction. Used for both the main and refund transaction --- libwallet/src/api_impl/foreign.rs | 132 +++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 123066a5d..48192416d 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -138,7 +138,137 @@ where Ok(ret_slate) } -/// +/// Receive an atomic tx as recipient +pub fn receive_atomic_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest_acct_name: Option<&str>, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut ret_slate = slate.clone(); + check_ttl(w, &ret_slate)?; + let parent_key_id = match dest_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.to_owned())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + // Don't do this multiple times + let tx = updater::retrieve_txs( + w, + None, + Some(ret_slate.id), + Some(&parent_key_id), + use_test_rng, + )?; + for t in &tx { + if t.tx_type == TxLogEntryType::TxReceived { + return Err(ErrorKind::TransactionAlreadyReceived(ret_slate.id.to_string()).into()); + } + } + + ret_slate.tx = Some(Slate::empty_transaction()); + + let height = w.last_confirmed_height()?; + let keychain = w.keychain(keychain_mask)?; + + let is_height_lock = ret_slate.kernel_features == 2; + // derive atomic secret from the slate's `atomic_id` + let atomic_secret = { + // FIXME: need a way to ensure atomic secrets are not reused + // + // The attack vector is being supplied/tricked into reusing the + // same derivation path for multiple swaps, where the swap has + // already been completed successfully. + // + // The attacker would not need to wait for the full signature + // in the repeat swap, since they would have the nonce already. + // + // Maybe use a bloom filter as an automated defense? + let atomic_id = match &ret_slate.atomic_id { + Some(aid) => aid.clone(), + None => return Err(ErrorKind::GenericError("missing atomic ID".into()).into()), + }; + Slate::check_atomic_id(&atomic_id)?; + let atomic = + keychain.derive_key(ret_slate.amount, &atomic_id, SwitchCommitmentType::Regular)?; + + let mut batch = w.batch(keychain_mask)?; + batch.save_atomic_secret(&atomic_id, &atomic)?; + batch.commit()?; + + Some(atomic) + }; + + let (input_ids, output_ids) = if is_height_lock { + // add input(s) and change output to slate + let ctx = tx::add_inputs_to_atomic_slate( + w, + keychain_mask, + &mut ret_slate, + height, + 10, // min_confirmations + 500, // max_outputs + 1, // num_change_outputs + true, // selection_strategy_is_use_all + &parent_key_id, + atomic_secret.clone(), + use_test_rng, + )?; + + (ctx.input_ids, ctx.output_ids) + } else { + (vec![], vec![]) + }; + + let mut context = tx::add_output_to_atomic_slate( + w, + keychain_mask, + &mut ret_slate, + height, + &parent_key_id, + atomic_scret, + use_test_rng, + )?; + + context.fee = Some(ret_slate.fee_fields.clone()); + let excess = ret_slate.calc_excess(keychain.secp())?; + + 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); + } + + if is_height_lock { + ret_slate.compact()?; + context.input_ids = input_ids; + context.output_ids.extend_from_slice(&output_ids); + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(ret_slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + ret_slate.adjust_offset(&keychain, &context)?; + ret_slate.state = SlateState::Atomic2; + + Ok(ret_slate) +} /// Receive a tx that this wallet has issued pub fn finalize_tx<'a, T: ?Sized, C, K>( From e720fc286814e23024c6abda98464e503d0df1a4 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 28 Apr 2021 02:08:12 +0000 Subject: [PATCH 12/17] Add API to countersign an atomic swap transaction Add function to complete the third round of the atomic swap, where the adaptor signature `s` is recovered. The adaptor signature is used with the full signature to recover the atomic nonce. --- libwallet/src/api_impl/owner.rs | 63 ++++++++++++++++++++++++++++++++- libwallet/src/internal/tx.rs | 46 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 7b6bdeba6..6053d992c 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -919,7 +919,14 @@ where ) } -/// Initialize sender atomic swap transaction +/// Initialize atomic swap transaction +/// +/// To initialize as the receiver for the `heigh_lock`ed +/// refund transaction, set the `late_lock` field in the +/// `InitTxArgs` +/// +/// Otherwise, the transaction will initialize as the sender +/// in the main transaction pub fn init_atomic_swap<'a, T: ?Sized, C, K>( w: &mut T, keychain_mask: Option<&SecretKey>, @@ -1036,6 +1043,60 @@ where Ok(slate) } +/// Complete the third round of the atomic swap +/// +/// In this round, the adaptor signature from round +/// two is saved, and the counterparty completes their +/// half of the kernel signature. +/// +/// For the refund transaction, the receiver is the counterparty. +/// +/// For the main transaction, the sender is the counterparty. +pub fn countersign_atomic_swap<'a, T: ?Sized, C, K>( + w: &mut T, + slate: &Slate, + keychain_mask: Option<&SecretKey>, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut ret_slate = slate.clone(); + check_ttl(w, &ret_slate)?; + + let mut context = w.get_private_context(keychain_mask, ret_slate.id.as_bytes())?; + + let keychain = w.keychain(keychain_mask)?; + // Save `s` from the adaptor signature to be able to extract the + // atomic secret using the full signature + // + // This is the atomic secret used to recover funds on the other chain + let adaptor_sig = ret_slate.fill_round_3_atomic(&keychain, &mut context)?; + let atomic_secret = SecretKey::from_slice(keychain.secp(), &adaptor_sig.as_ref()[32..])?; + + { + let atomic_id = + &ret_slate + .atomic_id + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing atomic ID".into(), + )))?; + let mut batch = w.batch(keychain_mask)?; + batch.save_atomic_secret(atomic_id, &atomic_secret)?; + batch.commit()?; + } + + if slate.kernel_features != 2 { + selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, true)?; + ret_slate.tx_or_err_mut()?.offset = ret_slate.offset.clone(); + } + ret_slate.state = SlateState::Atomic3; + + Ok(ret_slate) +} + /// Finalize slate pub fn finalize_tx<'a, T: ?Sized, C, K>( w: &mut T, diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 7942b742d..a2f6f4e69 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -266,6 +266,52 @@ where Ok(context) } +/// Add receiver output to the atomic slate +pub fn add_output_to_atomic_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + parent_key_id: &Identifier, + atomic_secret: Option, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = wallet.keychain(keychain_mask)?; + let is_initiator = atomic_secret.is_none(); + + // create an output using the amount in the slate + let (_, mut context, mut tx) = selection::build_recipient_output( + wallet, + keychain_mask, + slate, + current_height, + parent_key_id.clone(), + use_test_rng, + is_initiator, + )?; + + context.sec_atomic = atomic_scret; + // fill public keys + slate.fill_round_1(&keychain, &mut context)?; + // Create partial signature using the atomic secret, + // allows sender to recover the atomic secret when the + // full kernel signature is published + slate.fill_round_2_atomic(&keychain, &context)?; + // update excess in stored transaction + let mut batch = wallet.batch(keychain_mask)?; + tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + batch.save_private_context(slate.id.as_bytes(), &context, true)?; + batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; + batch.commit()?; + + Ok(context) +} + /// Create context, without adding inputs to slate pub fn create_late_lock_context<'a, T: ?Sized, C, K>( wallet: &mut T, From 267bcbc684bb35e59fa8dccaa9d02e7d5708b966 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 28 Apr 2021 04:22:25 +0000 Subject: [PATCH 13/17] Add APIs to complete an atomic swap Add foreign and owner APIs to complete an atomic swap main/refund transaction --- libwallet/src/api_impl/foreign.rs | 4 +++- libwallet/src/api_impl/owner.rs | 30 ++++++++++++++++++++++++++++++ libwallet/src/internal/tx.rs | 18 ++++++++++++++++++ libwallet/src/types.rs | 22 ++++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 48192416d..f0283388e 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -16,8 +16,8 @@ use rand::thread_rng; use strum::IntoEnumIterator; -use crate::api_impl::owner::finalize_tx as owner_finalize; use crate::api_impl::owner::{check_ttl, post_tx}; +use crate::api_impl::owner::{finalize_atomic_swap, finalize_tx as owner_finalize}; use crate::grin_core::core::FeeFields; use crate::grin_core::libtx::proof; use crate::grin_keychain::{Keychain, SwitchCommitmentType}; @@ -407,6 +407,8 @@ where let mut batch = w.batch(keychain_mask)?; batch.save_private_context(sl.id.as_bytes().as_ref(), &context)?; batch.commit()?; + } else if sl.state == SlateState::Atomic3 { + sl = finalize_atomic_swap(w, keychain_mask, slate)?; } else { sl = owner_finalize(w, keychain_mask, slate)?; } diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 6053d992c..d9291c5f9 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -1186,6 +1186,36 @@ where Ok(sl) } +/// Complete the atomic swap +pub fn finalize_atomic_swap<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut ret_slate = slate.clone(); + check_ttl(w, &ret_slate)?; + let context = w.get_private_context(keychain_mask, ret_slate.id.as_bytes())?; + let parent_key_id = w.parent_key_id(); + + if ret_slate.kernel_features == 2 { + ret_slate.tx = Some(Slate::empty_transaction()); + selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, true)?; + } + tx::complete_atomic_tx(&mut *w, keychain_mask, &mut ret_slate, &context)?; + tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &ret_slate)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &ret_slate, true)?; + + ret_slate.state = SlateState::Atomic4; + ret_slate.amount = 0; + + Ok(ret_slate) +} + /// cancel tx pub fn cancel_tx<'a, L, C, K>( wallet_inst: Arc>>>, diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index a2f6f4e69..1bf349cc1 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -402,6 +402,24 @@ where Ok(()) } +/// Complete an atomic swap transaction +pub fn complete_atomic_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Final transaction can be built by anyone at this stage + trace!("Slate to finalize is: {}", slate); + let _ = slate.finalize_atomic(&wallet.keychain(keychain_mask)?, context)?; + Ok(()) +} + /// Rollback outputs associated with a transaction in the wallet pub fn cancel_tx<'a, T: ?Sized, C, K>( wallet: &mut T, diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 5b667b807..fc9c8461e 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -239,6 +239,15 @@ where /// Next child ID when we want to create a new output, based on current parent fn next_child(&mut self, keychain_mask: Option<&SecretKey>) -> Result; + /// Return the current atomic secret index + fn current_atomic_id(&mut self) -> Result; + + /// Next atomic ID when we want to create a new atomic secret + fn next_atomic_id(&mut self, keychain_mask: Option<&SecretKey>) -> Result; + + /// Get the atomic ID for the atomic swap associated with the given UUID + fn get_used_atomic_id(&mut self, id: &Uuid) -> Result; + /// last verified height of outputs directly descending from the given parent key fn last_confirmed_height(&mut self) -> Result; @@ -283,6 +292,12 @@ where /// Save last stored child index of a given parent fn save_child_index(&mut self, parent_key_id: &Identifier, child_n: u32) -> Result<(), Error>; + /// Save global atomic index under the current keychain mask + fn save_atomic_index(&mut self, atomic_idx: u32) -> Result<(), Error>; + + /// Save an atomic index that has been used in an atomic swap + fn save_used_atomic_index(&mut self, id: &Uuid, atomic_idx: u32) -> Result<(), Error>; + /// Save last confirmed height of outputs for a given parent fn save_last_confirmed_height( &mut self, @@ -329,6 +344,13 @@ where atomic_id: &Identifier, secret: &SecretKey, ) -> Result<(), Error>; + + /// Save recovered secret for an atomic swap transaction + fn save_recovered_atomic_secret( + &mut self, + atomic_id: &Identifier, + secret: &SecretKey, + ) -> Result<(), Error>; } /// Encapsulate all wallet-node communication functions. No functions within libwallet From 19011fc2a0ad4db4205cc4d4862fe036fe322845 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 28 Apr 2021 04:24:50 +0000 Subject: [PATCH 14/17] Add function to recover atomic secret Add function to recover an atomic secret from a finalize transaction excess kernel signature, the initiator's partial signature, and the responder's adaptor signature --- libwallet/src/internal/tx.rs | 60 ++++++++++++++++++++++++++++++++++-- libwallet/src/lib.rs | 1 + 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 1bf349cc1..9d48ff2f1 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -19,7 +19,7 @@ use std::io::Cursor; use uuid::Uuid; use crate::grin_core::consensus::valid_header_version; -use crate::grin_core::core::HeaderVersion; +use crate::grin_core::core::{HeaderVersion, Transaction}; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; @@ -305,7 +305,7 @@ where // update excess in stored transaction let mut batch = wallet.batch(keychain_mask)?; tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); - batch.save_private_context(slate.id.as_bytes(), &context, true)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; batch.commit()?; @@ -420,6 +420,62 @@ where Ok(()) } +/// Recover atomic secret from final signature and adaptor signature +pub fn recover_atomic_secret<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + tx: &Transaction, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let full_sig = tx.kernels()[0].excess_sig.clone(); + let keychain = wallet.keychain(keychain_mask)?; + let secp = keychain.secp(); + + let context = wallet.get_private_context(keychain_mask, slate.id.as_bytes())?; + + let pdata_idx = slate.find_participant_data_index(secp, &context)?; + let part_sig = match slate.participant_data[pdata_idx].part_sig { + Some(ref s) => s, + None => { + return Err(ErrorKind::Signature( + "Could not recover partial signature from atomic swap slate".into(), + ) + .into()) + } + }; + + let mut sr = SecretKey::from_slice(secp, &full_sig.as_ref()[32..])?; + // Use the initiator's partial signature to recover the responder's partial + // signature from the full signature + let mut sp_s = SecretKey::from_slice(secp, &part_sig.as_ref()[32..])?; + // The atomic_secret contains sr' from the responder's adaptor signature + let atomic_id = wallet.get_used_atomic_id(&slate.id)?; + let mut srp = wallet.get_recovered_atomic_nonce(keychain_mask, &atomic_id)?; + + // Subtract the initiator's partial signature from the full signature + sp_s.neg_assign(secp)?; + sr.add_assign(secp, &sp_s)?; + + // Now the signature only contains the responder's partial signature + // calculate sr' - sr to recover the atomic secret + // x = (kr + x + rr*xr) - (kr + rr*xr) + sr.neg_assign(secp)?; + srp.add_assign(secp, &sr)?; + + { + // No longer need to keep the slate around, clean up + let mut batch = wallet.batch(keychain_mask)?; + batch.delete_private_context(slate.id.as_bytes())?; + } + + Ok(srp) +} + /// Rollback outputs associated with a transaction in the wallet pub fn cancel_tx<'a, T: ?Sized, C, K>( wallet: &mut T, diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 562ba5de8..d68acbd70 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -71,6 +71,7 @@ pub use api_impl::types::{ OutputCommitMapping, PaymentProof, VersionInfo, }; pub use internal::scan::scan; +pub use internal::tx::recover_atomic_secret; pub use slate_versions::ser as dalek_ser; pub use types::{ AcctPathMapping, BlockIdentifier, CbData, Context, NodeClient, NodeVersionInfo, OutputData, From 1cc59558659abb8e89187909fee1f399d4b16374 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 28 Apr 2021 23:01:45 +0000 Subject: [PATCH 15/17] Add atomic swap owner/foreign API functions --- api/src/foreign.rs | 43 +++ api/src/owner.rs | 31 ++ controller/tests/atomic.rs | 480 ++++++++++++++++++++++++++++++ libwallet/src/api_impl/foreign.rs | 2 +- libwallet/src/api_impl/owner.rs | 88 +++--- libwallet/src/internal/tx.rs | 57 +++- 6 files changed, 663 insertions(+), 38 deletions(-) create mode 100644 controller/tests/atomic.rs diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 5196862ec..9696aa07e 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -388,6 +388,49 @@ where } } + /// Receive an atomic swap transaction + pub fn receive_atomic_tx( + &self, + slate: &Slate, + dest_acct_name: Option<&str>, + r_addr: Option, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + if let Some(m) = self.middleware.as_ref() { + m( + ForeignCheckMiddlewareFn::ReceiveTx, + w.w2n_client().get_version_info(), + Some(slate), + )?; + } + let ret_slate = foreign::receive_atomic_tx( + &mut **w, + (&self.keychain_mask).as_ref(), + slate, + dest_acct_name, + self.doctest_mode, + )?; + match r_addr { + Some(a) => { + let tor_config_lock = self.tor_config.lock(); + let res = try_slatepack_sync_workflow( + &ret_slate, + &a, + tor_config_lock.clone(), + None, + true, + self.doctest_mode, + ); + match res { + Ok(s) => return Ok(s.unwrap()), + Err(_) => return Ok(ret_slate), + } + } + None => Ok(ret_slate), + } + } + /// Finalizes a (standard or invoice) transaction initiated by this wallet's Owner api. /// This step assumes the paying party has completed round 1 and 2 of slate /// creation, and added their partial signatures. This wallet will verify diff --git a/api/src/owner.rs b/api/src/owner.rs index 75f2f5f70..82c657242 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -913,6 +913,37 @@ where let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; owner::process_multisig_tx(&mut **w, keychain_mask, slate) + } + + /// Initializes an atomic swap transaction. The transaction can either be + /// the main or refund. To create a refund transaction, set `args.late_lock = Some(true)`. + pub fn init_atomic_swap( + &self, + keychain_mask: Option<&SecretKey>, + args: InitTxArgs, + derive_path: u32, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::init_atomic_swap( + &mut **w, + keychain_mask, + args, + derive_path, + self.doctest_mode, + ) + } + + /// Countersign the atomic swap transaction. Creates the first partial signature + /// over the transaction that contributes to the kernel excess signature. + pub fn countersign_atomic_swap( + &self, + slate: &Slate, + keychain_mask: Option<&SecretKey>, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::countersign_atomic_swap(&mut **w, slate, keychain_mask) } /// Locks the outputs associated with the inputs to the transaction in the given diff --git a/controller/tests/atomic.rs b/controller/tests/atomic.rs new file mode 100644 index 000000000..7ac3f5e8b --- /dev/null +++ b/controller/tests/atomic.rs @@ -0,0 +1,480 @@ +// Copyright 2021 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. + +//! Test wallets performing an atomic swap +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_wallet_libwallet as libwallet; +use grin_wallet_util::grin_core as core; +use grin_wallet_util::grin_keychain::{Keychain, SwitchCommitmentType}; + +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, Slate, SlateState, TxFlow}; +use std::thread; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// atomic swap impl +fn atomic_tx_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, + true + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + true + ); + 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 + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + Ok(()) + })?; + + let mut slate = Slate::blank(2, TxFlow::Atomic); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates the main atomic swap transaction + let args = InitTxArgs { + amount: 10, + ..Default::default() + }; + slate = api.init_atomic_swap(m, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic1); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_atomic_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic2); + + // Get the receiver's atomic secret created in `receive_atomic_tx` + // This is one of the keys locking the multisig transaction on the other chain + // Only revealed if the refund transaction is fully signed + posted + let atomic_secret = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let atomic_id = w.get_used_atomic_id(&slate.id)?; + w.keychain(mask2).unwrap().derive_key( + slate.amount, + &atomic_id, + SwitchCommitmentType::Regular, + )? + }; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + // wallet 1 creates the first partial signature on the atomic swap + slate = api.countersign_atomic_swap(&slate, m)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic3); + + // wallet 2 finalizes and posts the atomic swap + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic4); + + let rec_atomic_secret = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let tx = slate.tx_or_err()?; + libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx)? + }; + + assert_eq!(rec_atomic_secret, atomic_secret); + + Ok(()) +} + +/// atomic swap refund impl +fn atomic_refund_tx_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, + true + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + true + ); + 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 + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + Ok(()) + })?; + + let mut slate = Slate::blank(2, TxFlow::Atomic); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { + // Wallet 2 inititates the refund atomic swap transaction + let args = InitTxArgs { + amount: 10, + late_lock: Some(true), + ..Default::default() + }; + let derive_path = 0; + slate = api.init_atomic_swap(m, args, derive_path)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic1); + + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = api.receive_atomic_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic2); + + // Get the sender's atomic secret created in `receive_atomic_tx` + // This is one of the keys locking the multisig transaction on the other chain + // Only revealed if the refund transaction is fully signed + posted + let atomic_secret = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let atomic_id = w.get_used_atomic_id(&slate.id)?; + w.keychain(mask1)? + .derive_key(slate.amount, &atomic_id, SwitchCommitmentType::Regular)? + }; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { + // wallet 1 creates the first partial signature on the atomic swap + slate = api.countersign_atomic_swap(&slate, m)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic3); + + // wallet 2 finalizes and posts the atomic swap + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + api.tx_lock_outputs(m, &slate)?; + slate = api.finalize_atomic_swap(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic4); + + let rec_atomic_secret = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let tx = slate.tx_or_err()?; + libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx)? + }; + + assert_eq!(rec_atomic_secret, atomic_secret); + + Ok(()) +} + +/// atomic swap end-to-end impl +fn atomic_end_to_end_tx_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, + true + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + true + ); + 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 + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + Ok(()) + })?; + + let mut slate = Slate::blank(2, TxFlow::Atomic); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { + // Wallet 2 inititates the refund atomic swap transaction + let args = InitTxArgs { + amount: 10, + late_lock: Some(true), + ..Default::default() + }; + let derive_path = 0; + slate = api.init_atomic_swap(m, args, derive_path)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic1); + + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = api.receive_atomic_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic2); + + // Get the sender's atomic secret created in `receive_atomic_tx` + // This is one of the keys locking the multisig transaction on the other chain + // Only revealed if the refund transaction is fully signed + posted + let _atomic_secret = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let id = w.get_used_atomic_id(&slate.id)?; + w.keychain(mask1)? + .derive_key(slate.amount, &id, SwitchCommitmentType::Regular)? + }; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { + // wallet 1 creates the first partial signature on the atomic swap + slate = api.countersign_atomic_swap(&slate, m)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic3); + + /* Don't finalize and lock funds, since this locks the outputs used for the main transaction + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + api.tx_lock_outputs(m, &slate)?; + slate = api.finalize_atomic_swap(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic4); + + let rec_atomic_secret = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let tx = slate.tx_or_err()?; + libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx)? + }; + + assert_eq!(rec_atomic_secret, atomic_secret); + */ + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates the main atomic swap transaction + let args = InitTxArgs { + amount: 10, + ..Default::default() + }; + slate = api.init_atomic_swap(m, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic1); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_atomic_tx(&slate, None, None)?; + Ok(()) + })?; + + // Create atomic secret, this is created during the refund transaction + // This is one of the keys locking the multisig transaction on the other chain + let atomic_secret = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let atomic_id = w.get_used_atomic_id(&slate.id)?; + w.keychain(mask2)? + .derive_key(slate.amount, &atomic_id, SwitchCommitmentType::Regular)? + }; + + assert_eq!(slate.state, SlateState::Atomic2); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + // wallet 1 creates the first partial signature on the atomic swap + slate = api.countersign_atomic_swap(&slate, m)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic3); + + // wallet 2 finalizes and posts the atomic swap + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Atomic4); + + let rec_atomic_secret = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let tx = slate.tx_or_err()?; + libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx)? + }; + + assert_eq!(rec_atomic_secret, atomic_secret); + + Ok(()) +} + +#[test] +fn wallet_atomic_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/atomic_tx"; + setup(test_dir); + atomic_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn wallet_atomic_refund_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/atomic_refund_tx"; + setup(test_dir); + atomic_refund_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn wallet_atomic_end_to_end_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/atomic_end_to_end_tx"; + setup(test_dir); + atomic_end_to_end_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index f0283388e..64d73590c 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -237,7 +237,7 @@ where &mut ret_slate, height, &parent_key_id, - atomic_scret, + atomic_secret, use_test_rng, )?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index d9291c5f9..799d763a4 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -965,19 +965,8 @@ where let height = w.w2n_client().get_chain_tip()?.0; let keychain = w.keychain(keychain_mask)?; - // FIXME: need a way to ensure atomic secrets are not reused - // - // The attack vector is being supplied/tricked into reusing the - // same derivation path for multiple swaps, where the swap has - // already been completed successfully. - // - // The attacker would not need to wait for the full signature - // in the repeat swap, since they would have the nonce already. - // - // Maybe use a bloom filter as an automated defense? - let atomic_id = Slate::create_atomic_id(derive_path as u64); - slate.atomic_id = Some(atomic_id); - let mut context = if args.late_lock.unwrap_or(false) { + + let context = if args.late_lock.unwrap_or(false) { // use late_lock context for initial height_lock tx, // initiated by the atomic swap receiver let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); @@ -1014,21 +1003,6 @@ where context }; - // Payment Proof, add addresses to slate and save address - if let Some(a) = args.payment_proof_recipient_address { - let sec_addr_key = - address::address_from_derivation_path(&keychain, &parent_key_id, derive_path)?; - let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; - - slate.payment_proof = Some(PaymentInfo { - sender_address: sender_address.to_ed25519()?, - receiver_address: a.pub_key, - receiver_signature: None, - }); - - context.payment_proof_derivation_index = Some(derive_path); - } - // Save the aggsig context in our DB for when we // receive the transaction back { @@ -1076,15 +1050,24 @@ where let atomic_secret = SecretKey::from_slice(keychain.secp(), &adaptor_sig.as_ref()[32..])?; { - let atomic_id = - &ret_slate - .atomic_id + let atomic_id = w.next_atomic_id(keychain_mask)?; + let atomic = + keychain.derive_key(slate.amount, &atomic_id, SwitchCommitmentType::Regular)?; + let pub_atomic = PublicKey::from_secret_key(keychain.secp(), &atomic)?; + + debug!( + "Your public atomic key: {}", + pub_atomic + .serialize_vec(keychain.secp(), true) .as_ref() - .ok_or(Error::from(ErrorKind::GenericError( - "missing atomic ID".into(), - )))?; + .to_hex() + ); + debug!("Use this key to lock funds on the other chain.\n"); + let mut batch = w.batch(keychain_mask)?; - batch.save_atomic_secret(atomic_id, &atomic_secret)?; + batch.save_recovered_atomic_secret(&atomic_id, &atomic)?; + let atomic_idx = Slate::atomic_id_to_int(&atomic_id)?; + batch.save_used_atomic_index(&slate.id, atomic_idx)?; batch.commit()?; } @@ -1313,6 +1296,41 @@ where } } +/// Recover atomic secret from an adaptor signature and finalized kernel excess signature +pub fn recover_atomic_secret<'a, L, C, K>( + wallet_inst: &mut Arc>>>, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut w_lock = wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let mut client = w.w2n_client().clone(); + let keychain = w.keychain(keychain_mask)?; + if let Some((kernel, _, _)) = client.get_kernel( + &slate.calc_excess(keychain.secp())?, + Some(slate.ttl_cutoff_height.saturating_sub(60)), + Some(slate.ttl_cutoff_height), + )? { + let atomic = tx::recover_atomic_secret(&mut **w, keychain_mask, slate, &kernel)?; + let atomic_id = w.get_used_atomic_id(&slate.id)?; + info!("Saving atomic secret with atomic ID: {}, use with `get_atomic_secrets` to retrieve from storage", Slate::atomic_id_to_int(&atomic_id)?); + let mut batch = w.batch(keychain_mask)?; + batch.save_recovered_atomic_secret(&atomic_id, &atomic)?; + batch.commit()?; + Ok(atomic_id) + } else { + Err( + ErrorKind::StoredTx("missing finalized transaction kernel for the atomic swap".into()) + .into(), + ) + } +} + /// check repair /// Accepts a wallet inst instead of a raw wallet so it can /// lock as little as possible diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 9d48ff2f1..23cf922c5 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -138,6 +138,59 @@ where Ok((total, fee)) } +/// Add inputs to the slate (effectively becoming the sender) +pub fn add_inputs_to_atomic_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + atomic_secret: Option, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // sender should always refresh outputs + updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + let is_initiator = atomic_secret.is_none(); + + // Sender selects outputs into a new slate and save our corresponding keys in + // a transaction context. The secret key in our transaction context will be + // randomly selected. This returns the public slate, and a closure that locks + // our inputs and outputs once we're convinced the transaction exchange went + // according to plan + // This function is just a big helper to do all of that, in theory + // this process can be split up in any way + let mut context = selection::build_send_tx( + wallet, + &wallet.keychain(keychain_mask)?, + keychain_mask, + slate, + current_height, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + None, + parent_key_id.clone(), + use_test_rng, + is_initiator, + )?; + + context.sec_atomic = atomic_secret; + + context.initial_sec_key = context.sec_key.clone(); + + Ok(context) +} + /// Add inputs to the slate (effectively becoming the sender) pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( wallet: &mut T, @@ -295,7 +348,7 @@ where is_initiator, )?; - context.sec_atomic = atomic_scret; + context.sec_atomic = atomic_secret; // fill public keys slate.fill_round_1(&keychain, &mut context)?; // Create partial signature using the atomic secret, @@ -455,7 +508,7 @@ where let mut sp_s = SecretKey::from_slice(secp, &part_sig.as_ref()[32..])?; // The atomic_secret contains sr' from the responder's adaptor signature let atomic_id = wallet.get_used_atomic_id(&slate.id)?; - let mut srp = wallet.get_recovered_atomic_nonce(keychain_mask, &atomic_id)?; + let mut srp = wallet.get_recovered_atomic_secret(keychain_mask, &atomic_id)?; // Subtract the initiator's partial signature from the full signature sp_s.neg_assign(secp)?; From d85a4510747cb387273d669cda24dbdc22df9cc9 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Thu, 6 May 2021 23:30:20 +0000 Subject: [PATCH 16/17] Add atomic swap owner/foreign RPC functions --- api/src/foreign.rs | 2 +- api/src/foreign_rpc.rs | 286 ++++++++++++++++----- api/src/owner.rs | 174 ++++++++++++- api/src/owner_rpc.rs | 410 +++++++++++++++++++++++++----- api/tests/slate_versioning.rs | 7 +- controller/src/command.rs | 182 ++++++++++++- controller/tests/atomic.rs | 167 ++++++++++-- impls/src/adapters/http.rs | 72 ++++-- impls/src/backends/lmdb.rs | 169 +++++++++++- libwallet/src/api_impl/foreign.rs | 56 ++-- libwallet/src/api_impl/owner.rs | 13 +- libwallet/src/internal/tx.rs | 6 +- libwallet/src/slate.rs | 14 +- libwallet/src/types.rs | 7 + libwallet/tests/libwallet.rs | 2 +- src/bin/grin-wallet.yml | 146 +++++++++++ src/cmd/wallet_args.rs | 114 ++++++++- 17 files changed, 1600 insertions(+), 227 deletions(-) diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 9696aa07e..63c41aa94 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -419,7 +419,7 @@ where &a, tor_config_lock.clone(), None, - true, + false, self.doctest_mode, ); match res { diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index 2f285c88d..2bd897379 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -17,7 +17,7 @@ use crate::keychain::Keychain; use crate::libwallet::{ self, BlockFees, CbData, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, - NodeVersionInfo, Slate, SlateVersion, VersionInfo, VersionedCoinbase, VersionedSlate, + NodeVersionInfo, Slate, SlateVersion, TxFlow, VersionInfo, VersionedCoinbase, VersionedSlate, WalletLCProvider, }; use crate::{Foreign, ForeignCheckMiddlewareFn}; @@ -53,13 +53,14 @@ pub trait ForeignRpc { "Ok": { "foreign_api_version": 2, "supported_slate_versions": [ + "V5", "V4" ] } } } # "# - # ,false, 0, false, false); + # ,false, 0, false, TxFlow::Standard); ``` */ fn check_version(&self) -> Result; @@ -107,7 +108,7 @@ pub trait ForeignRpc { } } # "# - # ,false, 4, false, false); + # ,false, 4, false, TxFlow::Standard); ``` */ @@ -142,7 +143,7 @@ pub trait ForeignRpc { } ], "sta": "S1", - "ver": "4:2" + "ver": "5:2" }, null, null @@ -177,12 +178,12 @@ pub trait ForeignRpc { } ], "sta": "S2", - "ver": "4:2" + "ver": "5:2" } } } # "# - # ,false, 5, true, false); + # ,false, 5, true, TxFlow::Standard); ``` */ fn receive_tx( @@ -192,6 +193,85 @@ pub trait ForeignRpc { dest: Option, ) -> Result; + /** + ;Networked version of [Foreign::receive_atomic_tx](struct.Foreign.html#method.receive_atomic_tx). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_foreign_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "receive_atomic_tx", + "id": 1, + "params": [ + { + "amt": "6000000000", + "atomic_id": "046d7761746f6d69630000000000000000", + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sigs": [ + { + "nonce": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + } + ], + "multisig_path": "m/1018305059/3401844484/447546778/208817231", + "sta": "A1", + "ver": "4:2" + }, + null, + null + ] + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "6000000000", + "coms": [ + { + "c": "091582c92b99943b57955e52b5ccf1223780c2a2e55995c00c86fca2bcb46b6b9f", + "p": "49972a8d5b7c088e7813c3988ebe0982f8f0b12b849b1788df7da07b549408b0d6c99f80c0e2335370c104225ef5d282d79966e9044c959bedc3be03af6246fa07fc13eb3c60c90213c9f3a7a5ecf9a34c8fbaddc1a72e49e12dba9495e5aaa53bb6ac6ed63d8774707c57ab604d6bdc46de18da57a731fe336c3ccef92b4dae967417ffdae2c7d75864d46d30e287dd9cc15882e15f296b9bab0040e4432f4024be33924f112dd26c90cc800ac09a327b0ac3a661f63da9945fb1bcc82a7777d61d97cbe657675e22d035d2cf9ea03a89cfa410960ebc18a0a18b1909f4c5bef20b0fd13ffcf5a818ad8768d354b1c0f2e9b16dd7a9cf0641546f57d1945a98b8684d067dd085b90b40457e4c14665fb1b94feecf30a90f508ded16ba1bba8080a6866dffd0b1f01738fff8c62ce5e38e677835752a1b4072124dd9ff14ba8ff92126baebbb5f6e14fbb052f5d5b09aec11bfd880d7d4640a295aa83f184034d26f00cbdbabf9b89fddd7a7c9cc8c5d4b53fc39971e4495a8d984ac9607be89780fde528ee3f2d6b912908b4caf04f5c93f64431517af6b32d0b9c18255959f6903c6696ec71f615a0c877630a2d871f3f8a107fc80f306a94b6ad5790070f7d2535163bad7feae9263a9d3558ea1acecc4e61ff4e05b0162f6aba1a3b299ff1c3bb85e4109e550ad870c328bedc45fed8b504f679bc3c1a25b2b65ede44602f21fac123ba7c5f132e7c786bf9420a27bae4d2559cf7779e77f96b747b6d3ad5c13b5e8c9b49a7083001b2f98bcf242d4644537bb5a3b5b41764812a93395b7ab372c18be575e02c3763b4170234e5fddeb43420aadb71cb80f75cc681c1e7ffee3e6a8868c6076fd1da539ab9a12fef1c8cbe271b6de60100c9f82d826dc97b47b57ee9804e60112f556c1dce4f12ecc91ef34d69090b8c9d2ae9cbae38994a955cb" + } + ], + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sigs": [ + { + "nonce": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + }, + { + "atomic": "03e1d14f7b440af4944193b0559452651720ecf1847ef0f7092ef7e68414b8d732", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841bf9df0f2c4d96d6bb0b2e6e0b94ad6cbbc0e3f9e5582b085b31b5c8bb0e15bd36", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + } + ], + "sta": "A2", + "ver": "5:2" + } + } + } + # "# + # ,false, 5, true, TxFlow::Atomic); + ``` + */ + fn receive_atomic_tx( + &self, + slate: VersionedSlate, + dest_acct_name: Option, + dest: Option, + ) -> Result; + /** Networked version of [Foreign::finalize_tx](struct.Foreign.html#method.finalize_tx). @@ -206,7 +286,7 @@ pub trait ForeignRpc { "method": "finalize_tx", "id": 1, "params": [{ - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "I2", "off": "383bc9df0dd332629520a0a72f8dd7f0e97d579dccb4dbdc8592aa3d424c846c", @@ -276,12 +356,12 @@ pub trait ForeignRpc { } ], "sta": "I3", - "ver": "4:2" + "ver": "5:2" } } } # "# - # ,false, 5, false, true); + # ,false, 5, true, TxFlow::Invoice); ``` */ fn finalize_tx(&self, slate: VersionedSlate) -> Result; @@ -299,7 +379,7 @@ where fn build_coinbase(&self, block_fees: &BlockFees) -> Result { let cb: CbData = Foreign::build_coinbase(self, block_fees).map_err(|e| e.kind())?; - Ok(VersionedCoinbase::into_version(cb, SlateVersion::V4)) + Ok(VersionedCoinbase::into_version(cb, SlateVersion::V5)) } fn receive_tx( @@ -308,6 +388,7 @@ where dest_acct_name: Option, dest: Option, ) -> Result { + let v = in_slate.version(); let slate_from = Slate::from(in_slate); let out_slate = Foreign::receive_tx( self, @@ -316,13 +397,32 @@ where dest, ) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) + } + + fn receive_atomic_tx( + &self, + in_slate: VersionedSlate, + dest_acct_name: Option, + dest: Option, + ) -> Result { + let v = in_slate.version(); + let slate_from = Slate::from(in_slate); + let out_slate = Foreign::receive_atomic_tx( + self, + &slate_from, + dest_acct_name.as_ref().map(String::as_str), + dest, + ) + .map_err(|e| e.kind())?; + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) } fn finalize_tx(&self, in_slate: VersionedSlate) -> Result { + let v = in_slate.version(); let out_slate = Foreign::finalize_tx(self, &Slate::from(in_slate), true).map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) } } @@ -342,8 +442,8 @@ pub fn run_doctest_foreign( test_dir: &str, use_token: bool, blocks_to_mine: u64, - init_tx: bool, - init_invoice_tx: bool, + do_tx: bool, + tx_flow: TxFlow, ) -> Result, String> { use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; @@ -465,59 +565,128 @@ pub fn run_doctest_foreign( assert!(wallet_refreshed); } - if init_invoice_tx { - let amount = 60_000_000_000; - let mut slate = { - let mut w_lock = wallet2.lock(); - let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); - let args = IssueInvoiceTxArgs { - amount, - ..Default::default() - }; - api_impl::owner::issue_invoice_tx(&mut **w, (&mask2).as_ref(), args, true).unwrap() - }; - slate = { - let mut w_lock = wallet1.lock(); - let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); - let args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: 2, - max_outputs: 500, - num_change_outputs: 1, - selection_strategy_is_use_all: true, - ..Default::default() - }; - api_impl::owner::process_invoice_tx(&mut **w, (&mask1).as_ref(), &slate, args, true) - .unwrap() - }; - println!("INIT INVOICE SLATE"); - // Spit out slate for input to finalize_tx - println!("{}", serde_json::to_string_pretty(&slate).unwrap()); - } - - if init_tx { - let amount = 60_000_000_000; + if tx_flow == TxFlow::Atomic { + let amount = 60_012_500_000; let mut w_lock = wallet1.lock(); let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); let args = InitTxArgs { src_acct_name: None, amount, - minimum_confirmations: 2, + minimum_confirmations: 0, max_outputs: 500, num_change_outputs: 1, selection_strategy_is_use_all: true, + is_multisig: Some(true), ..Default::default() }; - let slate = api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); - println!("INIT SLATE"); - // Spit out slate for input to finalize_tx - println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + let mut sl = api_impl::owner::init_send_tx(&mut **w, mask1.as_ref(), args, true).unwrap(); + { + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + sl = + api_impl::foreign::receive_tx(&mut **w2, mask2.as_ref(), &sl, None, false).unwrap(); + + // Spit out slate for input to finalize_tx + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); + + sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); + } + + let _ = api_impl::owner::finalize_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + } + + if do_tx { + match tx_flow { + TxFlow::Invoice => { + let amount = 60_000_000_000; + let mut slate = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = IssueInvoiceTxArgs { + amount, + ..Default::default() + }; + api_impl::owner::issue_invoice_tx(&mut **w, (&mask2).as_ref(), args, true) + .unwrap() + }; + slate = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + api_impl::owner::process_invoice_tx( + &mut **w, + (&mask1).as_ref(), + &slate, + args, + true, + ) + .unwrap() + }; + println!("INIT INVOICE SLATE"); + // Spit out slate for input to finalize_tx + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + TxFlow::Standard => { + let amount = 60_000_000_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = + api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + println!("INIT SLATE"); + // Spit out slate for input to finalize_tx + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + TxFlow::Atomic => { + let amount = 60_000_000_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 0, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + multisig_path: Some("m/3489303641/867659429/4142508075/4116169848".into()), + ..Default::default() + }; + let slate = + api_impl::owner::init_atomic_swap(&mut **w, (&mask1).as_ref(), args, true) + .unwrap(); + println!("INIT SLATE"); + // Spit out slate for input to finalize_tx + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + TxFlow::Multisig => { + unimplemented!("multisig"); + } + } } - let mut api_foreign = match init_invoice_tx { - false => Foreign::new(wallet1, mask1, Some(test_check_middleware), true), - true => Foreign::new(wallet2, mask2, Some(test_check_middleware), true), + let mut api_foreign = match tx_flow { + TxFlow::Standard | TxFlow::Atomic | TxFlow::Multisig => { + Foreign::new(wallet1, mask1, Some(test_check_middleware), true) + } + TxFlow::Invoice => Foreign::new(wallet2, mask2, Some(test_check_middleware), true), }; api_foreign.doctest_mode = true; let foreign_api = &api_foreign as &dyn ForeignRpc; @@ -529,11 +698,12 @@ pub fn run_doctest_foreign( #[doc(hidden)] #[macro_export] macro_rules! doctest_helper_json_rpc_foreign_assert_response { - ($request:expr, $expected_response:expr, $use_token:expr, $blocks_to_mine:expr, $init_tx:expr, $init_invoice_tx:expr) => { + ($request:expr, $expected_response:expr, $use_token:expr, $blocks_to_mine:expr, $do_tx:expr, $tx_flow:expr) => { // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return // json response. // In order to prevent leaking tempdirs, This function should not panic. use grin_wallet_api::run_doctest_foreign; + use grin_wallet_libwallet::TxFlow; use serde_json; use serde_json::Value; use tempfile::tempdir; @@ -553,8 +723,8 @@ macro_rules! doctest_helper_json_rpc_foreign_assert_response { dir, $use_token, $blocks_to_mine, - $init_tx, - $init_invoice_tx, + $do_tx, + $tx_flow, ) .unwrap() .unwrap(); diff --git a/api/src/owner.rs b/api/src/owner.rs index 82c657242..7ff208003 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -22,7 +22,7 @@ use crate::config::{TorConfig, WalletConfig}; use crate::core::global; use crate::impls::HttpSlateSender; use crate::impls::SlateSender as _; -use crate::keychain::{Identifier, Keychain}; +use crate::keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::{ @@ -32,7 +32,7 @@ use crate::libwallet::{ }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::SecretKey; -use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; +use crate::util::{from_hex, static_secp_instance, Mutex, ToHex, ZeroingString}; use grin_wallet_util::OnionV3Address; use std::convert::TryFrom; use std::sync::atomic::{AtomicBool, Ordering}; @@ -801,6 +801,7 @@ where /// /// // . . . /// // The slate has been recieved from the invoicer, somehow + /// # use grin_wallet_libwallet::TxFlow; /// # let slate = Slate::blank(2, TxFlow::Invoice); /// let args = InitTxArgs { /// src_acct_name: None, @@ -921,17 +922,30 @@ where &self, keychain_mask: Option<&SecretKey>, args: InitTxArgs, - derive_path: u32, ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::init_atomic_swap( - &mut **w, - keychain_mask, - args, - derive_path, - self.doctest_mode, - ) + let send_args = args.send_args.clone(); + let slate = owner::init_atomic_swap(&mut **w, keychain_mask, args, self.doctest_mode)?; + if let Some(sa) = send_args { + let tor_config_lock = self.tor_config.lock(); + let tc = tor_config_lock.clone(); + let tc = match tc { + Some(mut c) => { + c.skip_send_attempt = Some(sa.skip_tor); + Some(c) + } + None => None, + }; + let res = + try_slatepack_sync_workflow(&slate, &sa.dest, tc, None, false, self.doctest_mode); + match res { + Ok(s) => Ok(s.unwrap()), + Err(_) => Ok(slate), + } + } else { + Ok(slate) + } } /// Countersign the atomic swap transaction. Creates the first partial signature @@ -940,10 +954,29 @@ where &self, slate: &Slate, keychain_mask: Option<&SecretKey>, + r_addr: Option, ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::countersign_atomic_swap(&mut **w, slate, keychain_mask) + let slate = owner::countersign_atomic_swap(&mut **w, &slate, keychain_mask)?; + match r_addr { + Some(a) => { + let tor_config_lock = self.tor_config.lock(); + let res = try_slatepack_sync_workflow( + &slate, + &a, + tor_config_lock.clone(), + None, + false, + self.doctest_mode, + ); + match res { + Ok(s) => return Ok(s.unwrap()), + Err(_) => return Ok(slate), + } + } + None => Ok(slate), + } } /// Locks the outputs associated with the inputs to the transaction in the given @@ -1076,6 +1109,125 @@ where owner::finalize_tx(&mut **w, keychain_mask, slate) } + /// Finalizes an atomic swap transaction, after all parties + /// have filled in all rounds of Slate generation. This step adds + /// all participants partial signatures to create the final signature, + /// resulting in a final transaction that is ready to post to a node. + /// + /// Note that this function DOES NOT POST the transaction to a node + /// for validation. This is done in separately via the + /// [`post_tx`](struct.Owner.html#method.post_tx) function. + /// + /// This function also stores the final transaction in the user's wallet files for retrieval + /// via the [`get_stored_tx`](struct.Owner.html#method.get_stored_tx) function. + /// + /// Combined with the partial signature from the second round, the kernel signature is used + /// to recover the atomic secret for unlocking funds on the other chain. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). All + /// participants must have filled in both rounds, and the sender should have locked their + /// outputs (via the [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs) function). + /// + /// # Returns + /// * ``Ok([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html))` if successful, + /// containing the new finalized slate. + /// * 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); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// let res = api_owner.tx_lock_outputs(None, &slate); + /// // + /// // Retrieve slate back from recipient + /// // + /// let res = api_owner.finalize_atomic_swap(None, &slate); + /// } + /// ``` + pub fn finalize_atomic_swap( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::finalize_atomic_swap(&mut **w, keychain_mask, slate) + } + + /// Recover the atomic secret from the second round adaptor signature, and the finalized kernel + /// excess signature. Use the atomic secret to recover funds on the other chain + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using + /// * `slate` - Third round atomic swap transaction slate with the initiator's partial signature + /// + /// # Returns + /// * `Ok(())` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + pub fn recover_atomic_secret( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result<(), Error> { + let atomic_id = + owner::recover_atomic_secret(&mut self.wallet_inst.clone(), keychain_mask, slate)?; + self.get_atomic_secrets( + keychain_mask, + Slate::atomic_id_to_int(&atomic_id)?, + slate.amount, + ) + } + + /// Recover the atomic secret from the second round adaptor signature, and the finalized kernel + /// excess signature. Use the atomic secret to recover funds on the other chain + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using + /// * `id` - Unique atomic swap identifier to recover stored atomic secrets + /// + /// # Returns + /// * `Ok(())` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + pub fn get_atomic_secrets( + &self, + keychain_mask: Option<&SecretKey>, + id: u32, + amount: u64, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let atomic_id = Slate::create_atomic_id(id); + let keychain = w.keychain(keychain_mask)?; + let our_nonce = keychain.derive_key(amount, &atomic_id, SwitchCommitmentType::Regular)?; + let rec_nonce = w.get_recovered_atomic_secret(keychain_mask, &atomic_id)?; + info!("Your atomic secret:"); + info!("{}\n", our_nonce.to_hex()); + info!("Recovered atomic secret:"); + info!("{}\n", rec_nonce.to_hex()); + Ok(()) + } + /// Posts a completed transaction to the listening node for validation and inclusion in a block /// for mining. /// diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index f1d116616..8af666047 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -20,7 +20,7 @@ use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::{ AcctPathMapping, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, + OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack, SlatepackAddress, StatusMessage, TxFlow, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; @@ -73,7 +73,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ fn accounts(&self, token: Token) -> Result, ErrorKind>; @@ -106,7 +106,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ fn create_account_path(&self, token: Token, label: &String) -> Result; @@ -139,7 +139,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ fn set_active_account(&self, token: Token, label: &String) -> Result<(), ErrorKind>; @@ -179,6 +179,7 @@ pub trait OwnerRpc { "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", "height": "1", "is_coinbase": true, + "is_multisig": false, "key_id": "0300000000000000000000000000000000", "lock_height": "4", "mmr_index": null, @@ -195,6 +196,7 @@ pub trait OwnerRpc { "commit": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", "height": "2", "is_coinbase": true, + "is_multisig": false, "key_id": "0300000000000000000000000100000000", "lock_height": "5", "mmr_index": null, @@ -210,7 +212,7 @@ pub trait OwnerRpc { } } # "# - # , 2, false, false, false, false); + # , 2, false, false, false, false, false); ``` */ fn retrieve_outputs( @@ -295,7 +297,7 @@ pub trait OwnerRpc { } } # "# - # , 2, false, false, false, false); + # , 2, false, false, false, false, false); ``` */ @@ -347,7 +349,7 @@ pub trait OwnerRpc { } } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ @@ -406,12 +408,12 @@ pub trait OwnerRpc { } ], "sta": "S1", - "ver": "4:2" + "ver": "5:2" } } } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ @@ -453,12 +455,12 @@ pub trait OwnerRpc { } ], "sta": "I1", - "ver": "4:2" + "ver": "5:2" } } } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ @@ -490,7 +492,7 @@ pub trait OwnerRpc { } ], "sta": "I1", - "ver": "4:2" + "ver": "5:2" }, "args": { "src_acct_name": null, @@ -536,12 +538,12 @@ pub trait OwnerRpc { } ], "sta": "I2", - "ver": "4:2" + "ver": "5:2" } } } # "# - # , 4, false, false, false, false); + # , 4, false, false, false, false, false); ``` */ @@ -576,7 +578,7 @@ pub trait OwnerRpc { "params": { "token": "d202964900000000d302964900000000d402964900000000d502964900000000", "slate": { - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "S1", "off": "d202964900000000d302964900000000d402964900000000d502964900000000", @@ -602,12 +604,162 @@ pub trait OwnerRpc { } } # "# - # , 5 ,true, false, false, false); + # , 5 ,true, false, false, false, false); ``` */ fn tx_lock_outputs(&self, token: Token, slate: VersionedSlate) -> Result<(), ErrorKind>; + /** + ;Networked version of [Owner::init_atomic_swap](struct.Owner.html#method.init_atomic_swap). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "init_atomic_swap", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "args": { + "src_acct_name": null, + "amount": "5000000000", + "minimum_confirmations": 0, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "target_slate_version": null, + "payment_proof_recipient_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", + "ttl_blocks": null, + "multisig_path": "m/1018305059/3401844484/447546778/208817231", + "send_args": null + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "6000000000", + "fee": "23000000", + "id": "0436430c-2b02-624c-2032-570501212b01", + "multisig_key_id": "043cb21a23cac407041aad059a0c724c4f", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "A1", + "ver": "5:2" + } + } + } + # "# + # , 4, false, true, false, false, true, false); + ``` + */ + fn init_atomic_swap(&self, token: Token, args: InitTxArgs) + -> Result; + + /** + Networked version of [Owner::countersign_atomic_swap](struct.Owner.html#method.countersign_atomic_swap). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "countersign_atomic_swap", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": { + "ver": "5:2", + "id": "0436430c-2b02-624c-2032-570501212b01", + "sta": "A2", + "off": "a5a632f26f27a9b71e98c1c8b8098bb41204ffcfd206d995f9c16d10764ad95a", + "amt": "50000000000", + "fee": "23500000", + "sigs": [ + { + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + }, + { + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "atomic": "03f94457808f7e1e68e07fe7dd0c43414220c1656573428c1b616175714f9d7c85", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b5205d0410c300529f342722369713adb64819dcbebee633f023e97cf3116a1a8" + } + ], + "coms": [ + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + } + ], + "atomic_id": "046d7761746f6d69630000000000000000" + }, + "r_addr": null + } + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "50000000000", + "coms": [ + { + "c": "09632f4e0fdec7e7113aee155113e591c5e55e0e000a879eca63e44835057a4d13", + "f": 2 + }, + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + } + ], + "fee": "12500000", + "id": "0436430c-2b02-624c-2032-570501212b01", + "off": "98b0ec495f87dabacee15e3af88b316179bbd26e0481ba49bc5c1d5a73c97a08", + "sigs": [ + { + "atomic": "03f94457808f7e1e68e07fe7dd0c43414220c1656573428c1b616175714f9d7c85", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b5205d0410c300529f342722369713adb64819dcbebee633f023e97cf3116a1a8", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + }, + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b04e1e15ceb1b5dbab8baf7750d7bd4aad6cfe97b83e4dc080dae328eb75881fd", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "A3", + "ver": "5:2" + } + } + } + # "# + # , 5 , true, true, false, false, true, false); + ``` + */ + fn countersign_atomic_swap( + &self, + token: Token, + slate: VersionedSlate, + r_addr: Option, + ) -> Result; + /** Networked version of [Owner::finalize_tx](struct.Owner.html#method.finalize_tx). @@ -622,7 +774,7 @@ pub trait OwnerRpc { "token": "d202964900000000d302964900000000d402964900000000d502964900000000", "slate": { - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "S2", "off": "6c6a69136154775488782121887bb3c32787a8320551fdb9732ec2d333fe54ee", @@ -684,12 +836,12 @@ pub trait OwnerRpc { } ], "sta": "S3", - "ver": "4:2" + "ver": "5:2" } } } # "# - # , 5, true, true, false, false); + # , 5, true, true, false, false, false); ``` */ fn finalize_tx(&self, token: Token, slate: VersionedSlate) @@ -708,7 +860,7 @@ pub trait OwnerRpc { "params": { "token": "d202964900000000d302964900000000d402964900000000d502964900000000", "slate": { - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "S3", "off": "750dbf4fd43b7f4cfd68d2698a522f3ff6e6a00ad9895b33f1ec46493b837b49", @@ -758,7 +910,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, false); + # , 5, true, true, true, false, false); ``` */ @@ -792,7 +944,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, false, false); + # , 5, true, true, false, false, false); ``` */ fn cancel_tx( @@ -836,12 +988,12 @@ pub trait OwnerRpc { "id": "0436430c-2b02-624c-2032-570501212b00", "sigs": [], "sta": "S3", - "ver": "4:3" + "ver": "5:3" } } } # "# - # , 5, true, true, false, false); + # , 5, true, true, false, false, false); ``` */ fn get_stored_tx( @@ -879,7 +1031,7 @@ pub trait OwnerRpc { } } # "# - # , 1, false, false, false, false); + # , 1, false, false, false, false, false); ``` */ fn scan( @@ -918,7 +1070,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false); + # , 5, false, false, false, false, false); ``` */ fn node_height(&self, token: Token) -> Result; @@ -1001,7 +1153,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false); + # , 5, false, false, false, false, false); ``` */ @@ -1031,7 +1183,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false); + # , 5, false, false, false, false, false); ``` */ @@ -1097,7 +1249,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false); + # , 5, false, false, false, false, false); ``` */ fn create_config( @@ -1135,7 +1287,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1172,7 +1324,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1202,7 +1354,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1233,7 +1385,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1265,7 +1417,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ fn change_password( @@ -1299,7 +1451,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ fn delete_wallet(&self, name: Option) -> Result<(), ErrorKind>; @@ -1329,7 +1481,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1357,7 +1509,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ fn stop_updater(&self) -> Result<(), ErrorKind>; @@ -1386,7 +1538,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1417,7 +1569,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1452,7 +1604,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1475,7 +1627,7 @@ pub trait OwnerRpc { "sender_index": 0, "recipients": [], "slate": { - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "S1", "off": "d202964900000000d302964900000000d402964900000000d502964900000000", @@ -1498,11 +1650,11 @@ pub trait OwnerRpc { "id": 1, "jsonrpc": "2.0", "result": { - "Ok": "BEGINSLATEPACK. xyfzdULuUuM5r3R kS68aywyCuYssPs Jf1JbvnBcK6NDDo ajiGAgh2SPx4t49 xtKuJE3BZCcSEue ksecMmbSoV2DQbX gGcmJniP9UadcmR N1KSc5FBhwAaUjy LXeYDP7EV7Cmsj4 pLaJdZTJTQbccUH 2zG8QTgoEiEWP5V T6rKst1TibmDAFm RRVHYDtskdYJb5G krqfpgN7RjvPfpm Z5ZFyz6ipAt5q9T 2HCjrTxkHdVi9js 22tr2Lx6iXT5vm8 JL6HhjwyFrSaEmN AjsBE8jgiaAABA6 GGZKwcXeXToMfRt nL9DeX1. ENDSLATEPACK." + "Ok": "BEGINSLATEPACK. 6PrWJrSxHAaYXif KSiSbTCJwgsconT yuny3iuyTxN6fd9 rYdUc3x5mTB3vRY sRFxNroQbChWXP4 5AG5JXPvkQezHGU wTLdoPdn5J1TfDe y5SwPMjbDN9Mm6a EraP2XYkcabg28z bic2YZfdNmJGWNa skPVRP3PtatQ4dk MpJ4qnn76w4uby3 EGzUDCJGtJRdbJK CByHc6B97qZLgL2 sKQ5fso4jiHc1xY YskK5J6FpoaDBK8 XRJyGTL9kQ79QUt WcVGDCYHiKpntkw BUmCJqE5iaVZEii zWN7hFh4G8yVfp9 WtdmYNxmM. ENDSLATEPACK." } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1557,7 +1709,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1599,7 +1751,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ @@ -1644,7 +1796,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, true); + # , 5, true, true, true, true, false); ``` */ @@ -1691,7 +1843,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, true); + # , 5, true, true, true, true, false); ``` */ @@ -1729,7 +1881,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false); + # , 0, false, false, false, false, false); ``` */ fn set_tor_config(&self, tor_config: Option) -> Result<(), ErrorKind>; @@ -1807,7 +1959,8 @@ where fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result { let slate = Owner::init_send_tx(self, (&token.keychain_mask).as_ref(), args) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) + let v = slate.version(); + Ok(VersionedSlate::into_version(slate, v).map_err(|e| e.kind())?) } fn issue_invoice_tx( @@ -1817,7 +1970,8 @@ where ) -> Result { let slate = Owner::issue_invoice_tx(self, (&token.keychain_mask).as_ref(), args) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) + let v = slate.version(); + Ok(VersionedSlate::into_version(slate, v).map_err(|e| e.kind())?) } fn process_invoice_tx( @@ -1833,7 +1987,8 @@ where args, ) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate, out_slate.version()).map_err(|e| e.kind())?) + let v = out_slate.version(); + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) } fn process_multisig_tx( @@ -1847,8 +2002,32 @@ where &Slate::from(in_slate), ) .map_err(|e| e.kind())?; - let version = SlateVersion::V5; - Ok(VersionedSlate::into_version(out_slate, version).map_err(|e| e.kind())?) + Ok(VersionedSlate::into_version(out_slate, SlateVersion::V5).map_err(|e| e.kind())?) + } + + fn init_atomic_swap( + &self, + token: Token, + args: InitTxArgs, + ) -> Result { + let out_slate = Owner::init_atomic_swap(self, (&token.keychain_mask).as_ref(), args) + .map_err(|e| e.kind())?; + let v = out_slate.version(); + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) + } + + fn countersign_atomic_swap( + &self, + token: Token, + in_slate: VersionedSlate, + r_addr: Option, + ) -> Result { + let slate = Slate::from(in_slate); + let out_slate = + Owner::countersign_atomic_swap(self, &slate, (&token.keychain_mask).as_ref(), r_addr) + .map_err(|e| e.kind())?; + let v = out_slate.version(); + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) } fn finalize_tx( @@ -1862,7 +2041,8 @@ where &Slate::from(in_slate), ) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(out_slate).map_err(|e| e.kind())?) + let v = out_slate.version(); + Ok(VersionedSlate::into_version(out_slate, v).map_err(|e| e.kind())?) } fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), ErrorKind> { @@ -1898,9 +2078,10 @@ where ) .map_err(|e| e.kind())?; match out_slate { - Some(s) => Ok(Some( - VersionedSlate::into_version(s, s.version()).map_err(|e| e.kind())?, - )), + Some(s) => Ok({ + let v = s.version(); + Some(VersionedSlate::into_version(s, v).map_err(|e| e.kind())?) + }), None => Ok(None), } } @@ -2101,7 +2282,8 @@ where secret_indices, ) .map_err(|e| e.kind())?; - Ok(VersionedSlate::into_version(slate, slate.version()).map_err(|e| e.kind())?) + let v = slate.version(); + Ok(VersionedSlate::into_version(slate, v).map_err(|e| e.kind())?) } fn decode_slatepack_message( @@ -2160,6 +2342,7 @@ pub fn run_doctest_owner( lock_tx: bool, finalize_tx: bool, payment_proof: bool, + countersign_atomic: bool, ) -> Result, String> { use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; @@ -2283,8 +2466,48 @@ pub fn run_doctest_owner( assert!(wallet_refreshed); } + if is_atomic && !perform_tx { + // Calculate the multisig output for the atomic swap + let amount = 50_012_500_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 0, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: None, + is_multisig: Some(true), + ..Default::default() + }; + let mut sl = api_impl::owner::init_send_tx(&mut **w, mask1.as_ref(), args, true).unwrap(); + { + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + sl = + api_impl::foreign::receive_tx(&mut **w2, mask2.as_ref(), &sl, None, false).unwrap(); + + // Spit out slate for input to finalize_tx + if lock_tx { + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); + } + + sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); + } + + let _ = api_impl::owner::finalize_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + } + if perform_tx { - let amount = 60_000_000_000; + let amount = if is_atomic { + 50_012_500_000 + } else { + 60_000_000_000 + }; let mut w_lock = wallet1.lock(); let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); let proof_address = match payment_proof { @@ -2305,15 +2528,59 @@ pub fn run_doctest_owner( payment_proof_recipient_address: proof_address, ..Default::default() }; - let mut slate = - api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + // Calculate the multisig output for the atomic swap + let sl = if is_atomic { + let mut pre_args = args.clone(); + pre_args.is_multisig = Some(true); + let mut sl = + api_impl::owner::init_send_tx(&mut **w, mask1.as_ref(), pre_args, true).unwrap(); + { + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + sl = api_impl::foreign::receive_tx(&mut **w2, mask2.as_ref(), &sl, None, false) + .unwrap(); + + // Spit out slate for input to finalize_tx + if lock_tx { + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); + } + + sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); + } + + api_impl::owner::finalize_tx(&mut **w, mask1.as_ref(), &sl).unwrap() + } else { + Slate::blank(2, TxFlow::Standard) + }; + let mut slate = if is_atomic { + let mut atom_args = args.clone(); + atom_args.amount = 50_000_000_000; + atom_args.minimum_confirmations = 0; + atom_args.multisig_path = Some(sl.create_multisig_id().to_bip_32_string()); + api_impl::owner::init_atomic_swap(&mut **w, (&mask1).as_ref(), atom_args, true).unwrap() + } else { + api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap() + }; println!("INITIAL SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); { let mut w_lock = wallet2.lock(); let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); - slate = api_impl::foreign::receive_tx(&mut **w2, (&mask2).as_ref(), &slate, None, true) - .unwrap(); + slate = if countersign_atomic { + api_impl::foreign::receive_atomic_tx( + &mut **w2, + (&mask2).as_ref(), + &slate, + None, + true, + ) + .unwrap() + } else { + api_impl::foreign::receive_tx(&mut **w2, (&mask2).as_ref(), &slate, None, true) + .unwrap() + }; w2.close().unwrap(); } // Spit out slate for input to finalize_tx @@ -2324,7 +2591,19 @@ pub fn run_doctest_owner( println!("RECEIPIENT SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); if finalize_tx { - slate = api_impl::owner::finalize_tx(&mut **w, (&mask1).as_ref(), &slate).unwrap(); + slate = if countersign_atomic { + let sl = + api_impl::owner::countersign_atomic_swap(&mut **w, &slate, (&mask1).as_ref()) + .unwrap(); + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let sl = api_impl::foreign::finalize_tx(&mut **w2, (&mask2).as_ref(), &sl, false) + .unwrap(); + w2.close().unwrap(); + sl + } else { + api_impl::owner::finalize_tx(&mut **w, (&mask1).as_ref(), &slate).unwrap() + }; error!("FINALIZED TX SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); } @@ -2357,7 +2636,7 @@ pub fn run_doctest_owner( #[doc(hidden)] #[macro_export] macro_rules! doctest_helper_json_rpc_owner_assert_response { - ($request:expr, $expected_response:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr) => { + ($request:expr, $expected_response:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr, $countersign_atomic:expr) => { // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return // json response. // In order to prevent leaking tempdirs, This function should not panic. @@ -2390,6 +2669,7 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response { $lock_tx, $finalize_tx, $payment_proof, + $countersign_atomic, ) .unwrap() .unwrap(); diff --git a/api/tests/slate_versioning.rs b/api/tests/slate_versioning.rs index 54cbf7392..50ca5331e 100644 --- a/api/tests/slate_versioning.rs +++ b/api/tests/slate_versioning.rs @@ -14,7 +14,8 @@ //! core::libtx specific tests use grin_wallet_api::foreign_rpc_client; use grin_wallet_api::run_doctest_foreign; -use grin_wallet_libwallet::{Slate, SlateVersion, VersionedSlate}; +use grin_wallet_libwallet::TxFlow; +//use grin_wallet_libwallet::{Slate, SlateVersion, VersionedSlate}; use serde_json; use serde_json::Value; use tempfile::tempdir; @@ -52,7 +53,7 @@ fn _receive_versioned_slate() { let request_val: Value = serde_json::from_str(v1_req).unwrap(); let expected_response: Value = serde_json::from_str(v1_resp).unwrap(); - let response = run_doctest_foreign(request_val, dir, false, 5, true, false) + let response = run_doctest_foreign(request_val, dir, false, 5, false, TxFlow::Standard) .unwrap() .unwrap(); @@ -83,7 +84,7 @@ fn receive_tx(vs: VersionedSlate) -> VersionedSlate { ) .unwrap(); let (call, tracker) = bound_method.call(); - let json_response = run_doctest_foreign(call.as_request(), dir, true, 5, false, false) + let json_response = run_doctest_foreign(call.as_request(), dir, 5, TxFlow::Standard) .unwrap() .unwrap(); let mut response = easy_jsonrpc_mw::Response::from_json_response(json_response).unwrap(); diff --git a/controller/src/command.rs b/controller/src/command.rs index c89597eb1..f063a5764 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -260,6 +260,7 @@ pub struct SendArgs { pub skip_tor: bool, pub outfile: Option, pub is_multisig: Option, + pub derive_path: Option, } pub fn send( @@ -269,13 +270,14 @@ pub fn send( args: SendArgs, dark_scheme: bool, test_mode: bool, + tx_flow: TxFlow, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: keychain::Keychain + 'static, { - let mut slate = Slate::blank(2, TxFlow::Standard); + let mut slate = Slate::blank(2, tx_flow.clone()); controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { if args.estimate_selection_strategies { let strategies = vec!["smallest", "all"] @@ -292,7 +294,12 @@ where is_multisig: args.is_multisig, ..Default::default() }; - let slate = api.init_send_tx(m, init_args)?; + let slate = match tx_flow { + TxFlow::Standard => api.init_send_tx(m, init_args)?, + TxFlow::Atomic => api.init_atomic_swap(m, init_args)?, + _ => api.init_send_tx(m, init_args)?, + }; + Ok((strategy, slate.amount, slate.fee_fields)) }) .collect::, grin_wallet_libwallet::Error>>()?; @@ -314,7 +321,11 @@ where is_multisig: args.is_multisig, ..Default::default() }; - let result = api.init_send_tx(m, init_args); + let result = match tx_flow { + TxFlow::Standard => api.init_send_tx(m, init_args), + TxFlow::Atomic => api.init_atomic_swap(m, init_args), + _ => api.init_send_tx(m, init_args), + }; slate = match result { Ok(s) => { info!( @@ -542,6 +553,7 @@ pub fn receive( args: ReceiveArgs, tor_config: Option, test_mode: bool, + tx_flow: TxFlow, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K>, @@ -569,7 +581,15 @@ where }; controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| { - slate = api.receive_tx(&slate, Some(&g_args.account), None)?; + slate = match tx_flow { + TxFlow::Standard => api.receive_tx(&slate, Some(&g_args.account), None)?, + TxFlow::Atomic => api.receive_atomic_tx(&slate, Some(&g_args.account), None)?, + _ => { + return Err( + libwallet::ErrorKind::GenericError("Invalid transaction flow".into()).into(), + ) + } + }; Ok(()) })?; @@ -605,6 +625,70 @@ where Err(e) => Err(e.into()), } } +pub fn countersign_atomic( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ReceiveArgs, + tor_config: Option, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (mut slate, ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let tor_config = match tor_config { + Some(mut c) => { + c.skip_send_attempt = Some(args.skip_tor); + Some(c) + } + None => None, + }; + + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + slate = api.countersign_atomic_swap(&slate, m, None)?; + Ok(()) + })?; + + let dest = match ret_address { + Some(a) => String::try_from(&a).unwrap(), + None => String::from(""), + }; + + let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + + match res { + Ok(Some(_)) => { + println!(); + println!( + "Transaction countersigned and sent back to {} for finalization.", + dest + ); + println!(); + Ok(()) + } + Ok(None) => { + output_slatepack( + owner_api, + keychain_mask, + &slate, + &dest, + args.outfile, + false, + false, + )?; + Ok(()) + } + Err(e) => Err(e.into()), + } +} /// Process Multisig command arguments #[derive(Clone)] @@ -777,6 +861,7 @@ pub fn finalize( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, args: FinalizeArgs, + tx_flow: TxFlow, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, @@ -806,7 +891,16 @@ where })?; } else { controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - slate = api.finalize_tx(m, &slate)?; + slate = match tx_flow { + TxFlow::Standard => api.finalize_tx(m, &slate)?, + TxFlow::Atomic => api.finalize_atomic_swap(m, &slate)?, + _ => { + return Err(libwallet::ErrorKind::GenericError( + "invalid transaction flow".into(), + ) + .into()) + } + }; Ok(()) })?; } @@ -857,6 +951,84 @@ where Ok(()) } +/// Recover atomic secret args +#[derive(Clone)] +pub struct RecoverAtomicArgs { + pub input_file: Option, + pub input_slatepack_message: Option, + pub outfile: Option, +} + +/// Recover the atomic secret from an adaptor signature and kernel excess signature +pub fn recover_atomic_secret( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: RecoverAtomicArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (slate, _ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file.clone(), + args.input_slatepack_message.clone(), + )?; + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let result = api.recover_atomic_secret(m, &slate); + match result { + Ok(_) => { + info!("Atomic nonce recovered successfully."); + Ok(()) + } + Err(e) => { + error!("Tx not sent: {}", e); + Err(e) + } + } + })?; + + println!("Transaction finalized successfully"); + Ok(()) +} + +/// Get atomic secrets args +#[derive(Clone)] +pub struct GetAtomicSecretsArgs { + pub id: u32, + pub amount: f64, +} + +/// Get atomic secrets from storage to recover funds on the other chain +pub fn get_atomic_secrets( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: GetAtomicSecretsArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let result = + api.get_atomic_secrets(m, args.id, (args.amount * (10_u64.pow(9) as f64)) as u64); + match result { + Ok(_) => { + info!("Atomic nonces recovered successfully."); + Ok(()) + } + Err(e) => { + error!("Tx not sent: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + /// Issue Invoice Args pub struct IssueInvoiceArgs { /// Slatepack address diff --git a/controller/tests/atomic.rs b/controller/tests/atomic.rs index 7ac3f5e8b..95f92cdbc 100644 --- a/controller/tests/atomic.rs +++ b/controller/tests/atomic.rs @@ -23,7 +23,7 @@ use grin_wallet_util::grin_keychain::{Keychain, SwitchCommitmentType}; use impls::test_framework::{self, LocalWalletClient}; use libwallet::{InitTxArgs, Slate, SlateState, TxFlow}; -use std::thread; +use std::{sync::atomic::Ordering, thread, time::Duration}; #[macro_use] mod common; @@ -34,6 +34,7 @@ fn atomic_tx_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(); + let stopper = wallet_proxy.running.clone(); create_wallet_and_add!( client1, @@ -98,7 +99,46 @@ fn atomic_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { // Wallet 1 inititates the main atomic swap transaction let args = InitTxArgs { - amount: 10, + amount: 5012500000, + is_multisig: Some(true), + ..Default::default() + }; + slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig1); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig2); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.process_multisig_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig3); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates the main atomic swap transaction + let args = InitTxArgs { + amount: 5000000000, + minimum_confirmations: 0, + multisig_path: Some(slate.create_multisig_id().to_bip_32_string()), ..Default::default() }; slate = api.init_atomic_swap(m, args)?; @@ -129,7 +169,7 @@ fn atomic_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { // wallet 1 creates the first partial signature on the atomic swap - slate = api.countersign_atomic_swap(&slate, m)?; + slate = api.countersign_atomic_swap(&slate, m, None)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic3); @@ -145,11 +185,14 @@ fn atomic_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let mut w_lock = wallet1.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; let tx = slate.tx_or_err()?; - libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx)? + libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx.kernels()[0])? }; assert_eq!(rec_atomic_secret, atomic_secret); + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) } @@ -158,6 +201,7 @@ fn atomic_refund_tx_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(); + let stopper = wallet_proxy.running.clone(); create_wallet_and_add!( client1, @@ -219,20 +263,62 @@ fn atomic_refund_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let mut slate = Slate::blank(2, TxFlow::Atomic); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates the main atomic swap transaction + let args = InitTxArgs { + amount: 5012500000, + is_multisig: Some(true), + ..Default::default() + }; + slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig1); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig2); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.process_multisig_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig3); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { // Wallet 2 inititates the refund atomic swap transaction let args = InitTxArgs { - amount: 10, + amount: 5000000000, late_lock: Some(true), + minimum_confirmations: 0, + multisig_path: Some(slate.create_multisig_id().to_bip_32_string()), ..Default::default() }; - let derive_path = 0; - slate = api.init_atomic_swap(m, args, derive_path)?; + slate = api.init_atomic_swap(m, args)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic1); wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + api.doctest_mode = true; slate = api.receive_atomic_tx(&slate, None, None)?; Ok(()) })?; @@ -251,7 +337,7 @@ fn atomic_refund_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { // wallet 1 creates the first partial signature on the atomic swap - slate = api.countersign_atomic_swap(&slate, m)?; + slate = api.countersign_atomic_swap(&slate, m, None)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic3); @@ -268,11 +354,14 @@ fn atomic_refund_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let mut w_lock = wallet2.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; let tx = slate.tx_or_err()?; - libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx)? + libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx.kernels()[0])? }; assert_eq!(rec_atomic_secret, atomic_secret); + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) } @@ -281,6 +370,7 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er // 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(); + let stopper = wallet_proxy.running.clone(); create_wallet_and_add!( client1, @@ -342,20 +432,58 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er let mut slate = Slate::blank(2, TxFlow::Atomic); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates the main atomic swap transaction + let args = InitTxArgs { + amount: 5012500000, + is_multisig: Some(true), + ..Default::default() + }; + slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig1); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_tx(&slate, None, None)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig2); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.process_multisig_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig3); + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Multisig4); + let multisig_path = slate.create_multisig_id().to_bip_32_string(); wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { // Wallet 2 inititates the refund atomic swap transaction let args = InitTxArgs { - amount: 10, + amount: 5000000000, late_lock: Some(true), + multisig_path: Some(multisig_path.clone()), ..Default::default() }; - let derive_path = 0; - slate = api.init_atomic_swap(m, args, derive_path)?; + slate = api.init_atomic_swap(m, args)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic1); wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + api.doctest_mode = true; slate = api.receive_atomic_tx(&slate, None, None)?; Ok(()) })?; @@ -374,7 +502,7 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er wallet::controller::owner_single_use(Some(wallet2.clone()), mask2.clone(), None, |api, m| { // wallet 1 creates the first partial signature on the atomic swap - slate = api.countersign_atomic_swap(&slate, m)?; + slate = api.countersign_atomic_swap(&slate, m, None)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic3); @@ -391,7 +519,7 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er let mut w_lock = wallet2.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; let tx = slate.tx_or_err()?; - libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx)? + libwallet::recover_atomic_secret(&mut **w, mask2, &slate, &tx.kernels()[0])? }; assert_eq!(rec_atomic_secret, atomic_secret); @@ -400,7 +528,9 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { // Wallet 1 inititates the main atomic swap transaction let args = InitTxArgs { - amount: 10, + amount: 500000000, + minimum_confirmations: 0, + multisig_path: Some(multisig_path), ..Default::default() }; slate = api.init_atomic_swap(m, args)?; @@ -428,7 +558,7 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er wallet::controller::owner_single_use(Some(wallet1.clone()), mask1.clone(), None, |api, m| { // wallet 1 creates the first partial signature on the atomic swap - slate = api.countersign_atomic_swap(&slate, m)?; + slate = api.countersign_atomic_swap(&slate, m, None)?; Ok(()) })?; assert_eq!(slate.state, SlateState::Atomic3); @@ -444,11 +574,14 @@ fn atomic_end_to_end_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Er let mut w_lock = wallet1.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; let tx = slate.tx_or_err()?; - libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx)? + libwallet::recover_atomic_secret(&mut **w, mask1, &slate, &tx.kernels()[0])? }; assert_eq!(rec_atomic_secret, atomic_secret); + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) } diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index 7155e8cca..58078e99a 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -15,7 +15,7 @@ /// HTTP Wallet 'plugin' implementation use crate::client_utils::{Client, ClientError, ClientErrorKind}; use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate}; -use crate::libwallet::{Error, ErrorKind, Slate}; +use crate::libwallet::{Error, ErrorKind, Slate, SlateState}; use crate::SlateSender; use serde::Serialize; use serde_json::{json, Value}; @@ -198,24 +198,58 @@ impl SlateSender for HttpSlateSender { }; // Note: not using easy-jsonrpc as don't want the dependencies in this crate let req = match finalize { - false => json!({ - "jsonrpc": "2.0", - "method": "receive_tx", - "id": 1, - "params": [ - slate_send, - null, - null - ] - }), - true => json!({ - "jsonrpc": "2.0", - "method": "finalize_tx", - "id": 1, - "params": [ - slate_send - ] - }), + false => match slate.state { + SlateState::Standard1 | SlateState::Invoice1 => json!({ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + slate_send, + null, + null + ] + }), + SlateState::Atomic1 => json!({ + "jsonrpc": "2.0", + "method": "receive_atomic_tx", + "id": 1, + "params": [ + slate_send, + null, + null + ] + }), + SlateState::Atomic2 => json!({ + "jsonrpc": "2.0", + "method": "countersign_atomic_swap", + "id": 1, + "params": [ + slate_send, + null, + null + ] + }), + _ => return Err(ErrorKind::ClientCallback("invalid slate state".into()).into()), + }, + true => match slate.state { + SlateState::Standard2 | SlateState::Invoice2 => json!({ + "jsonrpc": "2.0", + "method": "finalize_tx", + "id": 1, + "params": [ + slate_send + ] + }), + SlateState::Atomic3 => json!({ + "jsonrpc": "2.0", + "method": "finalize_atomic_swap", + "id": 1, + "params": [ + slate_send + ] + }), + _ => return Err(ErrorKind::ClientCallback("invalid slate state".into()).into()), + }, }; trace!("Sending receive_tx request: {}", req); diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs index 4dd0dbd6b..bebadc5c2 100644 --- a/impls/src/backends/lmdb.rs +++ b/impls/src/backends/lmdb.rs @@ -56,7 +56,9 @@ const LAST_SCANNED_BLOCK: u8 = b'l'; const LAST_SCANNED_KEY: &str = "LAST_SCANNED_KEY"; const WALLET_INIT_STATUS: u8 = b'w'; const WALLET_INIT_STATUS_KEY: &str = "WALLET_INIT_STATUS"; +const ATOMIC_ID_PREFIX: u8 = b'm'; const ATOMIC_SECRET_PREFIX: u8 = b's'; +const RECOVERED_ATOMIC_SECRET_PREFIX: u8 = b'r'; /// test to see if database files exist in the current directory. If so, /// use a DB backend for all operations @@ -135,6 +137,34 @@ where Ok(ret_atomic) } +fn recovered_atomic_xor_key( + keychain: &K, + atomic_id: &Identifier, +) -> Result<[u8; SECRET_KEY_SIZE], Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + //derive XOE value for storing public atomic secret + // h(root_key|atomic_id|"recovered_atomic_secret") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0); + hasher.update(&atomic_id.to_bytes()); + hasher.update(&b"recovered_atomic_secret"[..]); + let atomic_xor_key = hasher.finalize(); + let mut ret_atomic = [0; SECRET_KEY_SIZE]; + ret_atomic.copy_from_slice(&atomic_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + Ok(ret_atomic) +} + +fn default_parent_atomic_id() -> Identifier { + ExtKeychain::derive_key_id( + 2, 0x6d776174, /* 'mwat' */ + 0x6f6d6963, /* 'omic' */ + 0, 0, + ) +} + pub struct LMDBBackend<'ck, C, K> where C: NodeClient + 'ck, @@ -148,6 +178,8 @@ where pub master_checksum: Box>, /// Parent path to use by default for output operations parent_key_id: Identifier, + /// Atomic swap path to use by default for nonce operations + parent_atomic_id: Identifier, /// wallet to node client w2n_client: C, ///phantom @@ -193,6 +225,7 @@ where keychain: None, master_checksum: Box::new(None), parent_key_id: LMDBBackend::::default_path(), + parent_atomic_id: LMDBBackend::::default_atomic_path(), w2n_client: n_client, _phantom: &PhantomData, }; @@ -206,6 +239,13 @@ where ExtKeychain::derive_key_id(2, 0, 0, 0, 0) } + fn default_atomic_path() -> Identifier { + // return the default atomic secret wallet path, corresponding to the default account + // offset for atomic secret keys. Parent is account `hex(b"mwatomic")` at level 2, + // child atomic secret identifiers are all at level 3 + default_parent_atomic_id() + } + /// Just test to see if database files exist in the current directory. If /// so, use a DB backend for all operations pub fn exists(data_file_dir: &str) -> bool { @@ -509,6 +549,65 @@ where Ok(Identifier::from_path(&return_path)) } + fn current_atomic_id<'a>(&mut self) -> Result { + let index = { + let batch = self.db.batch()?; + let atomic_key = to_key( + ATOMIC_ID_PREFIX, + &mut self.parent_atomic_id.to_bytes().to_vec(), + ); + match batch.get_ser(&atomic_key)? { + Some(idx) => idx, + None => 0, + } + }; + let mut return_path = self.parent_atomic_id.to_path(); + return_path.depth += 1; + return_path.path[return_path.depth as usize - 1] = ChildNumber::from(index); + Ok(Identifier::from_path(&return_path)) + } + + fn next_atomic_id<'a>( + &mut self, + keychain_mask: Option<&SecretKey>, + ) -> Result { + let mut atomic_idx = { + let batch = self.db.batch()?; + let atomic_key = to_key( + ATOMIC_ID_PREFIX, + &mut self.parent_atomic_id.to_bytes().to_vec(), + ); + match batch.get_ser(&atomic_key)? { + Some(idx) => idx, + None => 0, + } + }; + let mut return_path = self.parent_atomic_id.to_path(); + return_path.depth += 1; + return_path.path[return_path.depth as usize - 1] = ChildNumber::from(atomic_idx); + atomic_idx += 1; + let mut batch = self.batch(keychain_mask)?; + batch.save_atomic_index(atomic_idx)?; + batch.commit()?; + Ok(Identifier::from_path(&return_path)) + } + + fn get_used_atomic_id(&mut self, id: &Uuid) -> Result { + let parent_atomic_id = self.parent_atomic_id.clone(); + let atomic_idx = { + let batch = self.db.batch()?; + let atomic_key = to_key(ATOMIC_ID_PREFIX, &mut id.as_bytes().to_vec()); + match batch.get_ser(&atomic_key)? { + Some(idx) => idx, + None => 0, + } + }; + let mut return_path = parent_atomic_id.to_path(); + return_path.depth += 1; + return_path.path[return_path.depth as usize - 1] = ChildNumber::from(atomic_idx); + Ok(Identifier::from_path(&return_path)) + } + fn last_confirmed_height<'a>(&mut self) -> Result { let batch = self.db.batch()?; let height_key = to_key( @@ -571,6 +670,30 @@ where } Ok(SecretKey::from_slice(keychain.secp(), xor_key.as_ref())?) } + + fn get_recovered_atomic_secret<'a>( + &mut self, + keychain_mask: Option<&SecretKey>, + atomic_id: &Identifier, + ) -> Result { + let keychain = self.keychain(keychain_mask)?; + let mut xor_key = recovered_atomic_xor_key(&keychain, atomic_id)?; + let nonce_key = to_key( + RECOVERED_ATOMIC_SECRET_PREFIX, + &mut atomic_id.to_bytes().to_vec(), + ); + let batch = self.db.batch()?; + let secret: Vec = match batch.get_ser(&nonce_key)? { + Some(s) => s, + None => { + return Err(ErrorKind::GenericError("missing atomic secret".into()).into()) + } + }; + for (x, s) in xor_key.iter_mut().zip(secret.iter()) { + *x ^= s; + } + Ok(SecretKey::from_slice(keychain.secp(), xor_key.as_ref())?) + } } /// An atomic batch in which all changes can be committed all at once or @@ -724,6 +847,27 @@ where Ok(()) } + fn save_atomic_index(&mut self, atomic_idx: u32) -> Result<(), Error> { + let parent_atomic_id = default_parent_atomic_id(); + let atomic_key = to_key(ATOMIC_ID_PREFIX, &mut parent_atomic_id.to_bytes().to_vec()); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&atomic_key, &atomic_idx)?; + Ok(()) + } + + fn save_used_atomic_index(&mut self, id: &Uuid, atomic_idx: u32) -> Result<(), Error> { + let atomic_key = to_key(ATOMIC_ID_PREFIX, &mut id.as_bytes().to_vec()); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&atomic_key, &atomic_idx)?; + Ok(()) + } + fn save_tx_log_entry( &mut self, tx_in: TxLogEntry, @@ -817,11 +961,32 @@ where fn save_atomic_secret( &mut self, atomic_id: &Identifier, - atomic_secret: &SecretKey, + secret: &SecretKey, ) -> Result<(), Error> { let mut xor_key = atomic_xor_key(self.keychain(), atomic_id)?; let secret_key = to_key(ATOMIC_SECRET_PREFIX, &mut atomic_id.to_bytes().to_vec()); - for (x, s) in xor_key.iter_mut().zip(atomic_secret.0[..].iter()) { + for (x, s) in xor_key.iter_mut().zip(secret.0[..].iter()) { + *x ^= s; + } + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&secret_key, &xor_key.to_vec())?; + Ok(()) + } + + fn save_recovered_atomic_secret( + &mut self, + atomic_id: &Identifier, + secret: &SecretKey, + ) -> Result<(), Error> { + let mut xor_key = recovered_atomic_xor_key(self.keychain(), atomic_id)?; + let secret_key = to_key( + RECOVERED_ATOMIC_SECRET_PREFIX, + &mut atomic_id.to_bytes().to_vec(), + ); + for (x, s) in xor_key.iter_mut().zip(secret.0[..].iter()) { *x ^= s; } self.db diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 64d73590c..96744aaf2 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -21,7 +21,7 @@ use crate::api_impl::owner::{finalize_atomic_swap, finalize_tx as owner_finalize use crate::grin_core::core::FeeFields; use crate::grin_core::libtx::proof; use crate::grin_keychain::{Keychain, SwitchCommitmentType}; -use crate::grin_util::from_hex; +use crate::grin_util::{from_hex, ToHex}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; use crate::internal::{selection, tx, updater}; @@ -183,31 +183,24 @@ where let keychain = w.keychain(keychain_mask)?; let is_height_lock = ret_slate.kernel_features == 2; - // derive atomic secret from the slate's `atomic_id` - let atomic_secret = { - // FIXME: need a way to ensure atomic secrets are not reused - // - // The attack vector is being supplied/tricked into reusing the - // same derivation path for multiple swaps, where the swap has - // already been completed successfully. - // - // The attacker would not need to wait for the full signature - // in the repeat swap, since they would have the nonce already. - // - // Maybe use a bloom filter as an automated defense? - let atomic_id = match &ret_slate.atomic_id { - Some(aid) => aid.clone(), - None => return Err(ErrorKind::GenericError("missing atomic ID".into()).into()), - }; - Slate::check_atomic_id(&atomic_id)?; + // derive atomic nonce from the slate's `atomic_id` + let (atomic_id, atomic_secret) = { + let atomic_id = w.next_atomic_id(keychain_mask)?; let atomic = keychain.derive_key(ret_slate.amount, &atomic_id, SwitchCommitmentType::Regular)?; - let mut batch = w.batch(keychain_mask)?; - batch.save_atomic_secret(&atomic_id, &atomic)?; - batch.commit()?; + let pub_atomic = PublicKey::from_secret_key(keychain.secp(), &atomic)?; - Some(atomic) + debug!( + "Your public atomic nonce: {}", + pub_atomic + .serialize_vec(keychain.secp(), true) + .as_ref() + .to_hex() + ); + debug!("Use this key to lock funds on the other chain.\n"); + + (atomic_id, Some(atomic)) }; let (input_ids, output_ids) = if is_height_lock { @@ -241,20 +234,15 @@ where use_test_rng, )?; - context.fee = Some(ret_slate.fee_fields.clone()); - let excess = ret_slate.calc_excess(keychain.secp())?; - - 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); + { + let atomic_idx = Slate::atomic_id_to_int(&atomic_id)?; + let mut batch = w.batch(keychain_mask)?; + batch.save_used_atomic_index(&ret_slate.id, atomic_idx)?; + batch.commit()?; } + context.fee = Some(ret_slate.fee_fields.clone()); + if is_height_lock { ret_slate.compact()?; context.input_ids = input_ids; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 799d763a4..10d1f43d0 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -28,7 +28,9 @@ use crate::api_impl::owner_updater::StatusMessage; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::internal::{keys, scan, selection, tx, updater}; use crate::slate::{KernelFeaturesArgs, PaymentInfo, Slate, SlateState, TxFlow}; -use crate::types::{AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletBackend, WalletInfo}; +use crate::types::{ + AcctPathMapping, NodeClient, OutputData, OutputStatus, TxLogEntry, WalletBackend, WalletInfo, +}; use crate::{ address, wallet_lock, Context, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, @@ -931,7 +933,6 @@ pub fn init_atomic_swap<'a, T: ?Sized, C, K>( w: &mut T, keychain_mask: Option<&SecretKey>, args: InitTxArgs, - derive_path: u32, use_test_rng: bool, ) -> Result where @@ -1065,16 +1066,18 @@ where debug!("Use this key to lock funds on the other chain.\n"); let mut batch = w.batch(keychain_mask)?; - batch.save_recovered_atomic_secret(&atomic_id, &atomic)?; + batch.save_recovered_atomic_secret(&atomic_id, &atomic_secret)?; let atomic_idx = Slate::atomic_id_to_int(&atomic_id)?; batch.save_used_atomic_index(&slate.id, atomic_idx)?; batch.commit()?; } - if slate.kernel_features != 2 { + ret_slate.adjust_offset(&keychain, &context)?; + + if ret_slate.kernel_features != 2 { selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, true)?; - ret_slate.tx_or_err_mut()?.offset = ret_slate.offset.clone(); } + ret_slate.state = SlateState::Atomic3; Ok(ret_slate) diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 23cf922c5..8588cac5f 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -19,7 +19,7 @@ use std::io::Cursor; use uuid::Uuid; use crate::grin_core::consensus::valid_header_version; -use crate::grin_core::core::{HeaderVersion, Transaction}; +use crate::grin_core::core::{HeaderVersion, TxKernel}; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; @@ -478,14 +478,14 @@ pub fn recover_atomic_secret<'a, T: ?Sized, C, K>( wallet: &mut T, keychain_mask: Option<&SecretKey>, slate: &Slate, - tx: &Transaction, + tx_kernel: &TxKernel, ) -> Result where T: WalletBackend<'a, C, K>, C: NodeClient + 'a, K: Keychain + 'a, { - let full_sig = tx.kernels()[0].excess_sig.clone(); + let full_sig = tx_kernel.excess_sig.clone(); let keychain = wallet.keychain(keychain_mask)?; let secp = keychain.secp(); diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index b663d3fe9..663247085 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -30,7 +30,7 @@ use crate::grin_keychain::{ use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; use crate::grin_util::secp::Signature; -use crate::grin_util::{secp, static_secp_instance}; +use crate::grin_util::{secp, static_secp_instance, ToHex}; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::Signature as DalekSignature; use serde::ser::{Serialize, Serializer}; @@ -582,6 +582,18 @@ impl Slate { let msg = self.msg_to_sign()?; let nonce_sum = self.pub_nonce_sum(secp)?; let key_sum = self.pub_blind_sum(secp)?; + let pub_atomic = self.participant_data[opdata_idx] + .public_atomic + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "Missing atomic public key".into(), + )))?; + + debug!( + "Other party's atomic public key: {}", + pub_atomic.serialize_vec(secp, true).as_ref().to_hex() + ); + debug!("Validate against the key used to lock funds on the other chain.\n"); aggsig::verify_partial_sig( secp, diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index fc9c8461e..1c9c97f37 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -263,6 +263,13 @@ where keychain_mask: Option<&SecretKey>, atomic_id: &Identifier, ) -> Result; + + /// Get the recovered secret for an atomic swap transaction + fn get_recovered_atomic_secret( + &mut self, + keychain_mask: Option<&SecretKey>, + atomic_id: &Identifier, + ) -> Result; } /// Batch trait to update the output data backend atomically. Trying to use a diff --git a/libwallet/tests/libwallet.rs b/libwallet/tests/libwallet.rs index fd686da8f..ca90d3ddc 100644 --- a/libwallet/tests/libwallet.rs +++ b/libwallet/tests/libwallet.rs @@ -619,7 +619,7 @@ fn test_atomic_swap_multisig_tx() { // set the atomic secret used for the adaptor signature let atomic_secret = SecretKey::from_slice(receiver_keychain.secp(), &[2; 32]).unwrap(); - receiver_ctx.set_atomic_secret(atomic_secret.clone()); + receiver_ctx.sec_atomic = Some(atomic_secret.clone()); // set kernel features, fee, and total amount to sign the correct kernel message slate.kernel_features = 0; diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 19fff32a6..49c54335b 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -277,6 +277,152 @@ subcommands: short: u long: outfile takes_value: true + - send_atomic: + about: Builds an atomic swap main transaction to send coins and sends to the recipient via the Slatepack workflow + args: + - refund: + help: Create an atomic swap refund transaction + short: r + long: refund + - amount: + help: Number of coins to send with optional fraction, e.g. 12.423 + index: 1 + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - selection_strategy: + help: Coin/Output selection strategy. + short: s + long: selection + possible_values: + - all + - smallest + default_value: smallest + takes_value: true + - estimate_selection_strategies: + help: Estimates all possible Coin/Output selection strategies. + short: e + long: estimate-selection + - late_lock: + help: EXPERIMENTAL - Do not lock the coins immediately, instead only lock them during finalization. + short: l + long: late-lock + - change_outputs: + help: Number of change outputs to generate (mainly for testing) + short: o + long: change_outputs + default_value: "1" + takes_value: true + - dest: + help: Intended recipient's Slatepack Address (or http listener address (DEPRECATED)) + short: d + long: dest + takes_value: true + - no_payment_proof: + help: Don't request a payment proof, even if the Recipient's Slatepack address is provided in the -dest argument + short: n + long: no_payment_proof + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - stored_tx: + help: If present, use the previously stored Unconfirmed transaction with given id + short: t + long: stored_tx + takes_value: true + - ttl_blocks: + help: If present, the number of blocks from the current after which wallets should refuse to process transactions further + short: b + long: ttl_blocks + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - receive_atomic: + about: Processes a Slatepack Message to accept an atomic swap transfer from a sender + args: + - input: + help: File containing a Slatepack Message + short: i + long: input + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - countersign_atomic: + about: Third round of an atomic swap, creates a signature, and recovers adaptor signature from the other party + args: + - input: + help: File containing a Slatepack Message + short: i + long: input + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - finalize_atomic: + about: Processes a Slatepack Message to finalize an atomic swap transfer. + args: + - input: + help: Partial transaction to process, expects the receiver's transaction file. + short: i + long: input + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - nopost: + help: Do not post the transaction. + short: n + long: nopost + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - recover_atomic_secret: + about: Processes a Slatepack Message to recover an atomic secret for recovering funds on the other chain. + args: + - input: + help: Finalized transaction to process, expects the initiator's transaction file. + short: i + long: input + takes_value: true + - get_atomic_secrets: + about: Uses the supplied ID to recover atomic secrets from storage + args: + - id: + help: Unique identifier to recover atomic swap secrets + short: i + long: id + takes_value: true + - amount: + help: Atomic swap amount + long: amount + takes_value: true - outputs: about: Raw wallet output info (list of outputs) - txs: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 8b5f6d2eb..bf8d313fa 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -26,7 +26,7 @@ use grin_wallet_config::{config_file_exists, TorConfig, WalletConfig}; use grin_wallet_controller::command; use grin_wallet_controller::{Error, ErrorKind}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; -use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor}; +use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor, TxFlow}; use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, WalletInst, WalletLCProvider}; use grin_wallet_util::grin_core as core; use grin_wallet_util::grin_core::core::amount_to_hr_string; @@ -289,6 +289,29 @@ fn parse_u64_or_none(arg: Option<&str>) -> Option { } } +fn parse_f64_or_none(arg: Option<&str>) -> Option { + let val = match arg { + Some(a) => a.parse::(), + None => return None, + }; + match val { + Ok(v) => Some(v), + Err(_) => None, + } +} + +// parses a number, returns None if argument is None, or value is absent +fn parse_u32_or_none(arg: Option<&str>) -> Option { + let val = match arg { + Some(a) => a.parse::(), + None => return None, + }; + match val { + Ok(v) => Some(v), + Err(_) => None, + } +} + pub fn parse_global_args( config: &WalletConfig, args: &ArgMatches, @@ -496,6 +519,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Result Result Result { + // input file + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + let outfile = parse_optional(args, "outfile")?; + + Ok(command::RecoverAtomicArgs { + input_file, + input_slatepack_message, + outfile, + }) +} + +pub fn parse_get_atomic_secrets_args( + args: &ArgMatches, +) -> Result { + let id = parse_u32_or_none(args.value_of("id")) + .ok_or(ParseError::ArgumentError("missing atomic ID".into()))?; + let amount = parse_f64_or_none(args.value_of("amount")).ok_or(ParseError::ArgumentError( + "missing atomic swap amount".into(), + ))?; + + Ok(command::GetAtomicSecretsArgs { id, amount }) +} + pub fn parse_unpack_args(args: &ArgMatches) -> Result { // input file let input_file = match args.is_present("input") { @@ -1156,6 +1224,7 @@ where a, wallet_config.dark_background_color_scheme.unwrap_or(true), test_mode, + TxFlow::Standard, ) } ("receive", Some(args)) => { @@ -1167,6 +1236,7 @@ where a, Some(tor_config.clone()), test_mode, + TxFlow::Standard, ) } ("unpack", Some(args)) => { @@ -1175,7 +1245,7 @@ where } ("finalize", Some(args)) => { let a = arg_parse!(parse_finalize_args(&args)); - command::finalize(owner_api, km, a) + command::finalize(owner_api, km, a, TxFlow::Standard) } ("invoice", Some(args)) => { let a = arg_parse!(parse_issue_invoice_args(&args)); @@ -1201,6 +1271,46 @@ where let a = arg_parse!(parse_process_multisig_args(&args)); command::process_multisig(owner_api, km, a, Some(tor_config.clone()), test_mode) } + ("send_atomic", Some(args)) => { + let a = arg_parse!(parse_send_args(&args)); + command::send( + owner_api, + km, + Some(tor_config.clone()), + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + test_mode, + TxFlow::Atomic, + ) + } + ("receive_atomic", Some(args)) => { + let a = arg_parse!(parse_receive_args(&args)); + command::receive( + owner_api, + km, + &global_wallet_args, + a, + Some(tor_config.clone()), + test_mode, + TxFlow::Atomic, + ) + } + ("countersign_atomic", Some(args)) => { + let a = arg_parse!(parse_receive_args(&args)); + command::countersign_atomic(owner_api, km, a, Some(tor_config.clone()), test_mode) + } + ("finalize_atomic", Some(args)) => { + let a = arg_parse!(parse_finalize_args(&args)); + command::finalize(owner_api, km, a, TxFlow::Atomic) + } + ("recover_atomic_secret", Some(args)) => { + let a = arg_parse!(parse_recover_atomic_args(&args)); + command::recover_atomic_secret(owner_api, km, a) + } + ("get_atomic_secrets", Some(args)) => { + let a = arg_parse!(parse_get_atomic_secrets_args(&args)); + command::get_atomic_secrets(owner_api, km, a) + } ("info", Some(args)) => { let a = arg_parse!(parse_info_args(&args)); command::info( From 11eb1127b63d0393881d81dce761236d143b0f87 Mon Sep 17 00:00:00 2001 From: Gene Ferneau Date: Wed, 23 Jun 2021 01:34:48 +0000 Subject: [PATCH 17/17] API Owner test for `process_multisig_tx` --- api/src/foreign_rpc.rs | 7 +- api/src/owner.rs | 4 +- api/src/owner_rpc.rs | 210 +++++++++--- api/tests/slate_versioning.rs | 2 +- controller/src/command.rs | 9 +- libwallet/src/api_impl/foreign.rs | 123 ++----- libwallet/src/api_impl/owner.rs | 156 ++++++--- libwallet/src/api_impl/types.rs | 3 + libwallet/src/internal/selection.rs | 192 +++++++---- libwallet/src/internal/tx.rs | 9 + libwallet/src/slate.rs | 18 +- libwallet/src/slate_versions/v5.rs | 8 + libwallet/src/slate_versions/v5_bin.rs | 16 +- libwallet/src/types.rs | 4 +- src/bin/grin-wallet.yml | 6 +- src/cmd/wallet_args.rs | 5 +- tests/cmd_line_atomic.rs | 452 +++++++++++++++++++++++++ tests/owner_v3_lifecycle.rs | 2 +- 18 files changed, 946 insertions(+), 280 deletions(-) create mode 100644 tests/cmd_line_atomic.rs diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index 2bd897379..03e4db56d 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -220,7 +220,7 @@ pub trait ForeignRpc { ], "multisig_path": "m/1018305059/3401844484/447546778/208817231", "sta": "A1", - "ver": "4:2" + "ver": "5:2" }, null, null @@ -588,9 +588,10 @@ pub fn run_doctest_foreign( // Spit out slate for input to finalize_tx println!("LOCKING TX"); - api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); + api_impl::owner::tx_lock_outputs(&mut **w, mask1.as_ref(), &sl).unwrap(); - sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = + api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl, false).unwrap(); sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); } diff --git a/api/src/owner.rs b/api/src/owner.rs index 7ff208003..c6bdb639d 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -913,8 +913,8 @@ where ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::process_multisig_tx(&mut **w, keychain_mask, slate) - } + owner::process_multisig_tx(&mut **w, keychain_mask, slate, self.doctest_mode) + } /// Initializes an atomic swap transaction. The transaction can either be /// the main or refund. To create a refund transaction, set `args.late_lock = Some(true)`. diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 8af666047..c305b74b9 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -73,7 +73,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ fn accounts(&self, token: Token) -> Result, ErrorKind>; @@ -106,7 +106,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ fn create_account_path(&self, token: Token, label: &String) -> Result; @@ -139,7 +139,7 @@ pub trait OwnerRpc { "id": 1 } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ fn set_active_account(&self, token: Token, label: &String) -> Result<(), ErrorKind>; @@ -212,7 +212,7 @@ pub trait OwnerRpc { } } # "# - # , 2, false, false, false, false, false); + # , 2, false, false, false, false, false, false); ``` */ fn retrieve_outputs( @@ -297,7 +297,7 @@ pub trait OwnerRpc { } } # "# - # , 2, false, false, false, false, false); + # , 2, false, false, false, false, false, false); ``` */ @@ -349,7 +349,7 @@ pub trait OwnerRpc { } } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ @@ -413,7 +413,7 @@ pub trait OwnerRpc { } } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ @@ -460,7 +460,7 @@ pub trait OwnerRpc { } } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ @@ -543,7 +543,7 @@ pub trait OwnerRpc { } } # "# - # , 4, false, false, false, false, false); + # , 4, false, false, false, false, false, false); ``` */ @@ -557,7 +557,66 @@ pub trait OwnerRpc { /** Networked version of [Owner::process_multisig_tx](struct.Owner.html#method.process_multisig_tx). - FIXME: add RPC doc-test + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "process_multisig_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": { + "amt": "5001250000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "028e95921cc0d5be5922362265d352c9bdabe51a9e1502a3f0d4a10387f1893f40", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5", + "part_commit": "091c34a190f7352149a343e3652d3f9e37972d822440e0d8ee03c658f003e178b4", + "tau_one": "02799bbf36460d56c1999bedd02acfefde8fd608f8b9ecc9bf02559a6d203cf308", + "tau_two": "020052d5842dc9a75859384b5136573545dc60553d766cad01f0665dbca937fb9a" + } + ], + "sta": "M3", + "ver": "5:2" + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "5001250000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5", + "part_commit": "091c34a190f7352149a343e3652d3f9e37972d822440e0d8ee03c658f003e178b4", + "tau_one": "0262ce501b38d17d3b378a198c94ab8dbbc7fbc6482543bea6d9f526350f874bd9", + "tau_two": "0204504dd81a04fbb104f3c5980dcbdc58f0b7084a721c760bbb47bc639b2e144d", + "tau_x": "3b77bcde9039b5219eabfd4b386981107659174bc1bf33283ee0ef1a2063456f", + "xs": "028e95921cc0d5be5922362265d352c9bdabe51a9e1502a3f0d4a10387f1893f40" + } + ], + "sta": "M3", + "ver": "5:2" + } + } + } + # "# + # , 5, false, true, false, false, false, true); + ``` */ fn process_multisig_tx( &self, @@ -604,7 +663,7 @@ pub trait OwnerRpc { } } # "# - # , 5 ,true, false, false, false, false); + # , 5 ,true, false, false, false, false, false); ``` */ @@ -630,6 +689,7 @@ pub trait OwnerRpc { "selection_strategy_is_use_all": true, "target_slate_version": null, "payment_proof_recipient_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", + "multisig_path": "m/860601635/303558731/534026549/1571534207", "ttl_blocks": null, "multisig_path": "m/1018305059/3401844484/447546778/208817231", "send_args": null @@ -645,7 +705,7 @@ pub trait OwnerRpc { "jsonrpc": "2.0", "result": { "Ok": { - "amt": "6000000000", + "amt": "5000000000", "fee": "23000000", "id": "0436430c-2b02-624c-2032-570501212b01", "multisig_key_id": "043cb21a23cac407041aad059a0c724c4f", @@ -841,7 +901,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, false, false, false); + # , 5, true, true, false, false, false, false); ``` */ fn finalize_tx(&self, token: Token, slate: VersionedSlate) @@ -910,7 +970,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, false, false); + # , 5, true, true, true, false, false, false); ``` */ @@ -944,7 +1004,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, false, false, false); + # , 5, true, true, false, false, false, false); ``` */ fn cancel_tx( @@ -993,7 +1053,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, false, false, false); + # , 5, true, true, false, false, false, false); ``` */ fn get_stored_tx( @@ -1031,7 +1091,7 @@ pub trait OwnerRpc { } } # "# - # , 1, false, false, false, false, false); + # , 1, false, false, false, false, false, false); ``` */ fn scan( @@ -1070,7 +1130,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false, false); + # , 5, false, false, false, false, false, false); ``` */ fn node_height(&self, token: Token) -> Result; @@ -1153,7 +1213,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false, false); + # , 5, false, false, false, false, false, false); ``` */ @@ -1183,7 +1243,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false, false); + # , 5, false, false, false, false, false, false); ``` */ @@ -1249,7 +1309,7 @@ pub trait OwnerRpc { } } # "# - # , 5, false, false, false, false, false); + # , 5, false, false, false, false, false, false); ``` */ fn create_config( @@ -1287,7 +1347,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1324,7 +1384,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1354,7 +1414,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1385,7 +1445,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1417,7 +1477,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ fn change_password( @@ -1451,7 +1511,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ fn delete_wallet(&self, name: Option) -> Result<(), ErrorKind>; @@ -1481,7 +1541,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1509,7 +1569,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ fn stop_updater(&self) -> Result<(), ErrorKind>; @@ -1538,7 +1598,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1569,7 +1629,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1604,7 +1664,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1654,7 +1714,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1709,7 +1769,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1751,7 +1811,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ @@ -1796,7 +1856,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, true, false); + # , 5, true, true, true, true, false, false); ``` */ @@ -1843,7 +1903,7 @@ pub trait OwnerRpc { } } # "# - # , 5, true, true, true, true, false); + # , 5, true, true, true, true, false, false); ``` */ @@ -1881,7 +1941,7 @@ pub trait OwnerRpc { } } # "# - # , 0, false, false, false, false, false); + # , 0, false, false, false, false, false, false); ``` */ fn set_tor_config(&self, tor_config: Option) -> Result<(), ErrorKind>; @@ -2342,7 +2402,8 @@ pub fn run_doctest_owner( lock_tx: bool, finalize_tx: bool, payment_proof: bool, - countersign_atomic: bool, + is_atomic: bool, + is_multisig: bool, ) -> Result, String> { use easy_jsonrpc_mw::Handler; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; @@ -2495,13 +2556,39 @@ pub fn run_doctest_owner( api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); } - sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = + api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl, false).unwrap(); sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); } let _ = api_impl::owner::finalize_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); } + if is_multisig { + let amount = 50_012_500_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 0, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: None, + is_multisig: Some(true), + ..Default::default() + }; + let mut sl = api_impl::owner::init_send_tx(&mut **w, mask1.as_ref(), args, true).unwrap(); + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + sl = api_impl::foreign::receive_tx(&mut **w2, mask2.as_ref(), &sl, None, false).unwrap(); + + // Spit out slate for input to finalize_tx + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); + } + if perform_tx { let amount = if is_atomic { 50_012_500_000 @@ -2526,6 +2613,7 @@ pub fn run_doctest_owner( num_change_outputs: 1, selection_strategy_is_use_all: true, payment_proof_recipient_address: proof_address, + is_multisig: Some(is_multisig), ..Default::default() }; // Calculate the multisig output for the atomic swap @@ -2541,12 +2629,11 @@ pub fn run_doctest_owner( .unwrap(); // Spit out slate for input to finalize_tx - if lock_tx { - println!("LOCKING TX"); - api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &sl).unwrap(); - } + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, mask1.as_ref(), &sl).unwrap(); - sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl).unwrap(); + sl = api_impl::owner::process_multisig_tx(&mut **w, mask1.as_ref(), &sl, false) + .unwrap(); sl = api_impl::foreign::finalize_tx(&mut **w2, mask2.as_ref(), &sl, false).unwrap(); } @@ -2568,7 +2655,7 @@ pub fn run_doctest_owner( { let mut w_lock = wallet2.lock(); let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); - slate = if countersign_atomic { + slate = if is_atomic { api_impl::foreign::receive_atomic_tx( &mut **w2, (&mask2).as_ref(), @@ -2578,8 +2665,14 @@ pub fn run_doctest_owner( ) .unwrap() } else { - api_impl::foreign::receive_tx(&mut **w2, (&mask2).as_ref(), &slate, None, true) - .unwrap() + api_impl::foreign::receive_tx( + &mut **w2, + (&mask2).as_ref(), + &slate, + None, + !is_multisig, + ) + .unwrap() }; w2.close().unwrap(); } @@ -2591,7 +2684,7 @@ pub fn run_doctest_owner( println!("RECEIPIENT SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); if finalize_tx { - slate = if countersign_atomic { + slate = if is_atomic { let sl = api_impl::owner::countersign_atomic_swap(&mut **w, &slate, (&mask1).as_ref()) .unwrap(); @@ -2601,6 +2694,20 @@ pub fn run_doctest_owner( .unwrap(); w2.close().unwrap(); sl + } else if is_multisig { + let sl = api_impl::owner::process_multisig_tx( + &mut **w, + (&mask1).as_ref(), + &slate, + false, + ) + .unwrap(); + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let sl = api_impl::foreign::finalize_tx(&mut **w2, (&mask2).as_ref(), &sl, false) + .unwrap(); + w2.close().unwrap(); + sl } else { api_impl::owner::finalize_tx(&mut **w, (&mask1).as_ref(), &slate).unwrap() }; @@ -2636,7 +2743,7 @@ pub fn run_doctest_owner( #[doc(hidden)] #[macro_export] macro_rules! doctest_helper_json_rpc_owner_assert_response { - ($request:expr, $expected_response:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr, $countersign_atomic:expr) => { + ($request:expr, $expected_response:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr, $is_atomic:expr, $is_multisig:expr) => { // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return // json response. // In order to prevent leaking tempdirs, This function should not panic. @@ -2669,7 +2776,8 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response { $lock_tx, $finalize_tx, $payment_proof, - $countersign_atomic, + $is_atomic, + $is_multisig, ) .unwrap() .unwrap(); diff --git a/api/tests/slate_versioning.rs b/api/tests/slate_versioning.rs index 50ca5331e..7bf69aaea 100644 --- a/api/tests/slate_versioning.rs +++ b/api/tests/slate_versioning.rs @@ -84,7 +84,7 @@ fn receive_tx(vs: VersionedSlate) -> VersionedSlate { ) .unwrap(); let (call, tracker) = bound_method.call(); - let json_response = run_doctest_foreign(call.as_request(), dir, 5, TxFlow::Standard) + let json_response = run_doctest_foreign(call.as_request(), dir, 5, TxFlow::Standard, false) .unwrap() .unwrap(); let mut response = easy_jsonrpc_mw::Response::from_json_response(json_response).unwrap(); diff --git a/controller/src/command.rs b/controller/src/command.rs index f063a5764..e75bef92b 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -261,6 +261,7 @@ pub struct SendArgs { pub outfile: Option, pub is_multisig: Option, pub derive_path: Option, + pub multisig_path: Option, } pub fn send( @@ -292,6 +293,7 @@ where selection_strategy_is_use_all: strategy == "all", estimate_only: Some(true), is_multisig: args.is_multisig, + multisig_path: args.multisig_path.clone(), ..Default::default() }; let slate = match tx_flow { @@ -319,6 +321,7 @@ where send_args: None, late_lock: Some(args.late_lock), is_multisig: args.is_multisig, + multisig_path: args.multisig_path.clone(), ..Default::default() }; let result = match tx_flow { @@ -926,11 +929,7 @@ where println!("Transaction finalized successfully"); - if slate - .participant_data - .iter() - .fold(false, |t, d| t | d.tau_x.is_some()) - { + if slate.is_multisig() { info!( "Transaction multisig identifier: {}", slate.create_multisig_id().to_bip_32_string() diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 96744aaf2..f6a320c54 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -13,17 +13,14 @@ // limitations under the License. //! Generic implementation of owner API functions -use rand::thread_rng; use strum::IntoEnumIterator; use crate::api_impl::owner::{check_ttl, post_tx}; use crate::api_impl::owner::{finalize_atomic_swap, finalize_tx as owner_finalize}; use crate::grin_core::core::FeeFields; -use crate::grin_core::libtx::proof; use crate::grin_keychain::{Keychain, SwitchCommitmentType}; -use crate::grin_util::{from_hex, ToHex}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; -use crate::grin_util::secp::pedersen::Commitment; +use crate::grin_util::ToHex; use crate::internal::{selection, tx, updater}; use crate::slate_versions::SlateVersion; use crate::{ @@ -110,8 +107,12 @@ where use_test_rng, )?; - // Add our contribution to the offset - ret_slate.adjust_offset(&keychain, &context)?; + let is_multisig = slate.is_multisig(); + + if !is_multisig { + // Add our contribution to the offset + ret_slate.adjust_offset(&keychain, &context)?; + } let excess = ret_slate.calc_excess(keychain.secp())?; @@ -126,12 +127,12 @@ where p.receiver_signature = Some(sig); } - ret_slate.amount = 0; - ret_slate.fee_fields = FeeFields::zero(); - ret_slate.remove_other_sigdata(&keychain, &context.sec_nonce, &context.sec_key)?; if ret_slate.is_multisig() { ret_slate.state = SlateState::Multisig2; } else { + ret_slate.amount = 0; + ret_slate.fee_fields = FeeFields::zero(); + ret_slate.remove_other_sigdata(&keychain, &context.sec_nonce, &context.sec_key)?; ret_slate.state = SlateState::Standard2; } @@ -203,6 +204,7 @@ where (atomic_id, Some(atomic)) }; + let min_confirmations = if use_test_rng { 0 } else { 10 }; let (input_ids, output_ids) = if is_height_lock { // add input(s) and change output to slate let ctx = tx::add_inputs_to_atomic_slate( @@ -210,7 +212,7 @@ where keychain_mask, &mut ret_slate, height, - 10, // min_confirmations + min_confirmations, 500, // max_outputs 1, // num_change_outputs true, // selection_strategy_is_use_all @@ -252,7 +254,6 @@ where batch.commit()?; } - ret_slate.adjust_offset(&keychain, &context)?; ret_slate.state = SlateState::Atomic2; Ok(ret_slate) @@ -293,74 +294,10 @@ where sl.state = SlateState::Invoice3; sl.amount = 0; } else if sl.state == SlateState::Multisig3 { + let tau_x = + selection::finalize_multisig_bulletproof(w, keychain_mask, &mut sl, &mut context)?; let k = w.keychain(keychain_mask)?; - let secp = k.secp(); - let (_, pub_nonce) = context.get_public_keys(secp); - - let tau_one = context.tau_one.ok_or(Error::from(ErrorKind::GenericError( - "missing tau one multisig key".into(), - )))?; - let tau_two = context.tau_two.ok_or(Error::from(ErrorKind::GenericError( - "missing tau two multisig key".into(), - )))?; - - { - let oth_data = sl - .participant_data - .iter() - .find(|d| d.public_nonce != pub_nonce) - .ok_or(Error::from(ErrorKind::GenericError( - "missing other participant's data".into(), - )))?; - let oth_tau_one = oth_data.tau_one.ok_or(Error::from(ErrorKind::GenericError( - "missing other tau one multisig key".into(), - )))?; - let oth_tau_two = oth_data.tau_two.ok_or(Error::from(ErrorKind::GenericError( - "missing other tau two multisig key".into(), - )))?; - - context.tau_one = Some(PublicKey::from_combination( - secp, - vec![&tau_one, &oth_tau_one], - )?); - context.tau_two = Some(PublicKey::from_combination( - secp, - vec![&tau_two, &oth_tau_two], - )?); - let common_nonce = context.create_common_nonce(secp, &oth_data.public_nonce)?; - let key_id = sl.create_multisig_id(); - let out = w.iter().find(|o| o.key_id == key_id).ok_or(Error::from( - ErrorKind::GenericError("missing multisig output".into()), - ))?; - - let commit_str = out - .commit - .as_ref() - .ok_or(Error::from(ErrorKind::GenericError( - "missing multisig output commit".into(), - )))?; - let commit_hex = from_hex(&commit_str) - .map_err(|e| ErrorKind::GenericError(format!("invalid hex: {}", e)))?; - let commit = Commitment::from_vec(commit_hex); - - // finish receiver's side of the multisig bulletproof - context.tau_x = Some(SecretKey::new(secp, &mut thread_rng())); - let _ = proof::create_multisig( - &k, - &proof::ProofBuilder::new(&k), - sl.amount, - &key_id, - SwitchCommitmentType::Regular, - &common_nonce, - context.tau_x.as_mut(), - context.tau_one.as_mut(), - context.tau_two.as_mut(), - &[commit], - 2, - None, - )?; - } - + let (_, pub_nonce) = context.get_public_keys(k.secp()); { let mut part_data = sl .participant_data @@ -369,28 +306,16 @@ where .ok_or(Error::from(ErrorKind::GenericError( "missing local participant data".into(), )))?; - part_data.tau_x = context.tau_x.clone(); + part_data.tau_x = tau_x; } - // FIXME: keep tau_one and tau_two in participant data to check on other end? - - let oth_tau_x = sl - .participant_data - .iter() - .find(|d| d.public_nonce != pub_nonce) - .ok_or(Error::from(ErrorKind::GenericError( - "missing other participant's data".into(), - )))? - .tau_x - .as_ref() - .ok_or(Error::from(ErrorKind::GenericError( - "missing other tau x key".into(), - )))?; - - // compute tau x sum to store in the backend DB - let tau_x = context.tau_x.as_mut().unwrap(); - tau_x.add_assign(secp, oth_tau_x)?; - - sl.state = SlateState::Multisig4; + + sl.adjust_offset(&k, &context)?; + + debug!( + "multisig ID path: {}", + slate.create_multisig_id().to_bip_32_string() + ); + debug!("Use with commands spending this multisig output"); let mut batch = w.batch(keychain_mask)?; batch.save_private_context(sl.id.as_bytes().as_ref(), &context)?; diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 10d1f43d0..86ff2d1ea 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::Transaction; -use crate::grin_core::libtx::proof; +use crate::grin_core::libtx::{proof, tx_fee}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::{Mutex, ToHex}; use crate::util::{OnionV3Address, OnionV3AddressError}; @@ -745,6 +745,7 @@ pub fn process_multisig_tx<'a, T: ?Sized, C, K>( w: &mut T, keychain_mask: Option<&SecretKey>, slate: &Slate, + use_test_rng: bool, ) -> Result where T: WalletBackend<'a, C, K>, @@ -758,13 +759,17 @@ where let secp = keychain.secp(); let key_id = slate.create_multisig_id(); let (_, pub_nonce) = context.get_public_keys(secp); - let oth_part_data = slate - .participant_data - .iter() - .find(|d| d.public_nonce != pub_nonce) - .ok_or(Error::from(ErrorKind::GenericError( - "missing other participant data".into(), - )))?; + let oth_part_data = if use_test_rng && slate.participant_data.len() == 1 { + &slate.participant_data[0] + } else { + slate + .participant_data + .iter() + .find(|d| d.public_nonce != pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant data".into(), + )))? + }; let oth_part_commit = oth_part_data @@ -796,27 +801,27 @@ where None, )?; - if oth_part_data.tau_one.is_none() || oth_part_data.tau_two.is_none() { - return Err(ErrorKind::GenericError( - "missing other participant's tau public key(s)".into(), - ) - .into()); - } + let (oth_tau_one, oth_tau_two) = match ( + oth_part_data.tau_one.as_ref(), + oth_part_data.tau_two.as_ref(), + ) { + (Some(one), Some(two)) => (one, two), + _ => { + return Err(ErrorKind::GenericError( + "missing other participant's tau public key(s)".into(), + ) + .into()) + } + }; // calculate tau_one_sum and tau_two_sum let mut tau_one_sum = Some(PublicKey::from_combination( secp, - vec![ - context.tau_one.as_ref().unwrap(), - oth_part_data.tau_one.as_ref().unwrap(), - ], + vec![context.tau_one.as_ref().unwrap(), oth_tau_one], )?); let mut tau_two_sum = Some(PublicKey::from_combination( secp, - vec![ - context.tau_two.as_ref().unwrap(), - oth_part_data.tau_two.as_ref().unwrap(), - ], + vec![context.tau_two.as_ref().unwrap(), oth_tau_two], )?); // calculate initiator's tau_x secret key for the multisig bulletproof @@ -951,6 +956,13 @@ where None => w.parent_key_id(), }; + let multisig_path_str = args + .multisig_path + .ok_or(Error::from(ErrorKind::GenericError( + "missing BIP32 multisig path".into(), + )))?; + let multisig_id = Identifier::from_bip_32_string(&multisig_path_str)?; + let mut slate = tx::new_tx_slate( &mut *w, args.amount, @@ -960,19 +972,25 @@ where args.ttl_blocks, )?; - if let Some(v) = args.target_slate_version { - slate.version_info.version = v; - }; + slate.multisig_key_id = Some(multisig_id.clone()); let height = w.w2n_client().get_chain_tip()?.0; let keychain = w.keychain(keychain_mask)?; + let output = w + .iter() + .find(|d| d.key_id == multisig_id) + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output".into(), + )))?; + let context = if args.late_lock.unwrap_or(false) { // use late_lock context for initial height_lock tx, // initiated by the atomic swap receiver let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); context.amount = args.amount; - context.fee = slate.fee_fields.as_opt(); + context.fee = Some((tx_fee(1, 1, 1) as u32).into()); + context.input_ids = vec![(output.key_id, output.mmr_index, output.value)]; slate.fill_round_1(&keychain, &mut context)?; @@ -1012,7 +1030,6 @@ where batch.commit()?; } - slate.adjust_offset(&keychain, &context)?; slate.compact()?; Ok(slate) @@ -1125,6 +1142,7 @@ where args.selection_strategy_is_use_all, Some(context.fee.map(|f| f.fee()).unwrap_or(0)), parent_key_id.clone(), + None, false, true, )?; @@ -1145,29 +1163,61 @@ where } // finalize multisig bulletproof - if sl - .participant_data - .iter() - .fold(false, |t, d| t | d.tau_x.is_some()) - { - selection::finalize_multisig_bulletproof(&mut *w, keychain_mask, &mut sl, &mut context)?; + let is_multisig = sl.is_multisig(); + let multisig_init_final = is_multisig && context.tau_x.is_some(); + if is_multisig { + let tau_x = + selection::finalize_multisig_bulletproof(w, keychain_mask, &mut sl, &mut context)?; + let k = w.keychain(keychain_mask)?; + let (_, pub_nonce) = context.get_public_keys(k.secp()); + { + let mut part_data = sl + .participant_data + .iter_mut() + .find(|d| d.public_nonce == pub_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing local participant data".into(), + )))?; + part_data.tau_x = tau_x; + } + } + + if multisig_init_final { + context + .output_ids + .push((sl.create_multisig_id(), None, sl.amount)); } // Add our contribution to the offset sl.adjust_offset(&keychain, &context)?; - selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; + if multisig_init_final { + context.output_ids.pop(); + } + + if multisig_init_final || !is_multisig { + selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; - tx::complete_tx(&mut *w, keychain_mask, &mut sl, &context)?; - tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; - tx::update_stored_tx(&mut *w, keychain_mask, &context, &sl, false)?; + tx::complete_tx(&mut *w, keychain_mask, &mut sl, &context)?; + tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &sl, false)?; + } { let mut batch = w.batch(keychain_mask)?; batch.delete_private_context(sl.id.as_bytes())?; batch.commit()?; } - sl.state = SlateState::Standard3; - sl.amount = 0; + if is_multisig { + debug!( + "multisig ID path: {}", + slate.create_multisig_id().to_bip_32_string() + ); + debug!("Use with commands spending this multisig output"); + sl.state = SlateState::Multisig4; + } else { + sl.state = SlateState::Standard3; + sl.amount = 0; + } Ok(sl) } @@ -1185,13 +1235,37 @@ where { let mut ret_slate = slate.clone(); check_ttl(w, &ret_slate)?; - let context = w.get_private_context(keychain_mask, ret_slate.id.as_bytes())?; + let mut context = w.get_private_context(keychain_mask, ret_slate.id.as_bytes())?; let parent_key_id = w.parent_key_id(); + let is_height_lock = ret_slate.kernel_features == 2; + + if !is_height_lock { + // check that the multisig ID matches a locally stored multisig output + let multisig_id = + slate + .multisig_key_id + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig ouptut ID".into(), + )))?; + let output = w + .iter() + .find(|o| &o.key_id == multisig_id) + .ok_or(Error::from(ErrorKind::GenericError( + "missing multisig output".into(), + )))?; + context.input_ids = vec![(output.key_id, output.mmr_index, output.value)]; + } - if ret_slate.kernel_features == 2 { + ret_slate.adjust_offset(&w.keychain(keychain_mask)?, &context)?; + + if is_height_lock { ret_slate.tx = Some(Slate::empty_transaction()); selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, true)?; } + + ret_slate.tx_or_err_mut()?.offset = ret_slate.offset.clone(); + tx::complete_atomic_tx(&mut *w, keychain_mask, &mut ret_slate, &context)?; tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &ret_slate)?; tx::update_stored_tx(&mut *w, keychain_mask, &context, &ret_slate, true)?; diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 58b39c380..fb7812039 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -81,6 +81,8 @@ pub struct InitTxArgs { /// If true, the transaction should contain a multisignature output shared by all the /// participants pub is_multisig: Option, + /// BIP32 path for the multisig output spent in an atomic swap transaction + pub multisig_path: Option, } /// Send TX API Args, for convenience functionality that inits the transaction and sends @@ -113,6 +115,7 @@ impl Default for InitTxArgs { late_lock: Some(false), send_args: None, is_multisig: None, + multisig_path: None, } } } diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 7e44164db..3710203c7 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -14,6 +14,8 @@ //! Selection of inputs for building transactions +use rand::thread_rng; + use crate::address; use crate::error::{Error, ErrorKind}; use crate::grin_core::core::{amount_to_hr_string, Output, OutputFeatures}; @@ -27,7 +29,7 @@ use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen; use crate::grin_util::{from_hex, ToHex}; use crate::internal::keys; -use crate::slate::Slate; +use crate::slate::{Slate, SlateState}; use crate::types::*; use crate::util::OnionV3Address; use std::collections::HashMap; @@ -50,6 +52,7 @@ pub fn build_send_tx<'a, T: ?Sized, C, K>( selection_strategy_is_use_all: bool, fixed_fee: Option, parent_key_id: Identifier, + multisig_key_id: Option<&Identifier>, use_test_nonce: bool, is_initiator: bool, ) -> Result @@ -68,6 +71,7 @@ where change_outputs, selection_strategy_is_use_all, &parent_key_id, + multisig_key_id, false, )?; @@ -278,18 +282,13 @@ where let (commit, output) = if is_multisig { let (_, public_nonce) = context.get_public_keys(keychain.secp()); - let data = if use_test_rng { - assert_eq!(slate.participant_data.len(), 1); - &slate.participant_data[0] - } else { - slate - .participant_data - .iter() - .find(|d| d.public_nonce != public_nonce) - .ok_or(Error::from(ErrorKind::GenericError( - "missing other participant data".into(), - )))? - }; + let data = slate + .participant_data + .iter() + .find(|d| d.public_nonce != public_nonce) + .ok_or(Error::from(ErrorKind::GenericError( + "missing other participant data".into(), + )))?; let oth_partial_commit = data.part_commit.ok_or(Error::from(ErrorKind::Commit( "missing partial commit".into(), @@ -355,12 +354,8 @@ where t.kernel_excess = Some(e) } t.kernel_lookup_min_height = Some(current_height); - let root_key_id = match is_multisig { - true => key_id.clone(), - false => parent_key_id.clone(), - }; batch.save(OutputData { - root_key_id: root_key_id, + root_key_id: parent_key_id.clone(), key_id: key_id_inner.clone(), mmr_index: None, n_child: key_id_inner.to_path().last_path_index(), @@ -392,6 +387,7 @@ pub fn select_send_tx<'a, T: ?Sized, C, K, B>( change_outputs: usize, selection_strategy_is_use_all: bool, parent_key_id: &Identifier, + multisig_key_id: Option<&Identifier>, include_inputs_in_sum: bool, ) -> Result< ( @@ -417,6 +413,7 @@ where change_outputs, selection_strategy_is_use_all, &parent_key_id, + multisig_key_id, )?; // build transaction skeleton with inputs and change @@ -443,6 +440,7 @@ pub fn select_coins_and_fee<'a, T: ?Sized, C, K>( change_outputs: usize, selection_strategy_is_use_all: bool, parent_key_id: &Identifier, + multisig_key_id: Option<&Identifier>, ) -> Result< ( Vec, @@ -466,6 +464,7 @@ where max_outputs, selection_strategy_is_use_all, parent_key_id, + multisig_key_id, ); // sender is responsible for setting the fee on the partial tx @@ -528,6 +527,7 @@ where max_outputs, selection_strategy_is_use_all, parent_key_id, + multisig_key_id, ) .1; fee = tx_fee(coins.len(), num_outputs, 1); @@ -627,6 +627,7 @@ pub fn select_coins<'a, T: ?Sized, C, K>( max_outputs: usize, select_all: bool, parent_key_id: &Identifier, + multisig_key_id: Option<&Identifier>, ) -> (usize, Vec) // max_outputs_available, Outputs where @@ -635,13 +636,15 @@ where K: Keychain + 'a, { // first find all eligible outputs based on number of confirmations - let mut eligible = wallet - .iter() - .filter(|out| { - out.root_key_id == *parent_key_id - && out.eligible_to_spend(current_height, minimum_confirmations) - }) - .collect::>(); + let key_id = multisig_key_id.unwrap_or(parent_key_id); + let mut eligible = vec![]; + for out in wallet.iter() { + if (out.root_key_id == *key_id || out.key_id == *key_id) + && out.eligible_to_spend(current_height, minimum_confirmations) + { + eligible.push(out.clone()); + } + } let max_available = eligible.len(); @@ -774,7 +777,7 @@ pub fn finalize_multisig_bulletproof<'a, T: ?Sized, C, K>( keychain_mask: Option<&SecretKey>, slate: &mut Slate, context: &mut Context, -) -> Result<(), Error> +) -> Result, Error> where T: WalletBackend<'a, C, K>, C: NodeClient + 'a, @@ -791,20 +794,6 @@ where "missing other participant data".into(), )))?; - let mut tau_x_sum = context - .tau_x - .clone() - .ok_or(Error::from(ErrorKind::GenericError( - "missing local tau x".into(), - )))?; - let oth_tau_x = oth_data - .tau_x - .as_ref() - .ok_or(Error::from(ErrorKind::GenericError( - "missing other tau x".into(), - )))?; - tau_x_sum.add_assign(secp, oth_tau_x)?; - let common_nonce = context.create_common_nonce(secp, &oth_data.public_nonce)?; let key_id = slate.create_multisig_id(); @@ -826,35 +815,98 @@ where .map_err(|e| ErrorKind::GenericError(format!("invalid hex: {}", e)))?; let commit = pedersen::Commitment::from_vec(commit_hex); - let proof = create_multisig( - &keychain, - &ProofBuilder::new(&keychain), - amount, - &key_id, - SwitchCommitmentType::Regular, - &common_nonce, - Some(&mut tau_x_sum), - context.tau_one.as_mut(), - context.tau_two.as_mut(), - &[commit.clone()], - 0, - None, - )? - .ok_or(Error::from(ErrorKind::GenericError( - "error creating final multisig proof".into(), - )))?; - - // replace the multisig output's rangeproof with the finalized multisig proof - let mut new_outs = vec![Output::new(OutputFeatures::Multisig, commit.clone(), proof)]; - let old_outs: Vec = slate - .tx_or_err()? - .outputs() - .iter() - .filter(|o| o.identifier.commit != commit) - .map(|o| o.clone()) - .collect(); - new_outs.extend_from_slice(&old_outs[..]); - slate.tx_or_err_mut()?.body.outputs = new_outs; + let is_initiator_final = context.tau_x.is_some(); + if !is_initiator_final { + let tau_one = context.tau_one.ok_or(Error::from(ErrorKind::GenericError( + "missing tau one multisig key".into(), + )))?; + let tau_two = context.tau_two.ok_or(Error::from(ErrorKind::GenericError( + "missing tau two multisig key".into(), + )))?; + let oth_tau_one = oth_data.tau_one.ok_or(Error::from(ErrorKind::GenericError( + "missing other tau one multisig key".into(), + )))?; + let oth_tau_two = oth_data.tau_two.ok_or(Error::from(ErrorKind::GenericError( + "missing other tau two multisig key".into(), + )))?; + context.tau_one = Some(PublicKey::from_combination( + secp, + vec![&tau_one, &oth_tau_one], + )?); + context.tau_two = Some(PublicKey::from_combination( + secp, + vec![&tau_two, &oth_tau_two], + )?); + context.tau_x = Some(SecretKey::new(secp, &mut thread_rng())); + let _ = create_multisig( + &keychain, + &ProofBuilder::new(&keychain), + amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + context.tau_x.as_mut(), + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit], + 2, + None, + )?; + } + let mut tau_x_sum = context + .tau_x + .clone() + .ok_or(Error::from(ErrorKind::GenericError( + "missing local tau x".into(), + )))?; + // Save for receiver to add to the slate, can be ignored for initiator finalization + let ret_tau_x = Some(tau_x_sum.clone()); + let oth_tau_x = oth_data + .tau_x + .as_ref() + .ok_or(Error::from(ErrorKind::GenericError( + "missing other tau x".into(), + )))?; + tau_x_sum.add_assign(secp, oth_tau_x)?; - Ok(()) + if is_initiator_final { + let proof = create_multisig( + &keychain, + &ProofBuilder::new(&keychain), + amount, + &key_id, + SwitchCommitmentType::Regular, + &common_nonce, + Some(&mut tau_x_sum), + context.tau_one.as_mut(), + context.tau_two.as_mut(), + &[commit.clone()], + 0, + None, + )? + .ok_or(Error::from(ErrorKind::GenericError( + "error creating final multisig proof".into(), + )))?; + + let output = Output::new(OutputFeatures::Multisig, commit.clone(), proof); + output.verify_proof()?; + + // replace the multisig output's rangeproof with the finalized multisig proof + let mut new_outs = vec![output]; + let old_outs: Vec = slate + .tx_or_err()? + .outputs() + .iter() + .filter(|o| o.identifier.commit != commit) + .map(|o| o.clone()) + .collect(); + new_outs.extend_from_slice(&old_outs[..]); + slate.tx_or_err_mut()?.body.outputs = new_outs; + } else { + context.tau_x = Some(tau_x_sum); + } + + slate.state = SlateState::Multisig4; + + Ok(ret_tau_x) } diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index 8588cac5f..87d13b306 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -134,6 +134,7 @@ where num_change_outputs, selection_strategy_is_use_all, parent_key_id, + None, )?; Ok((total, fee)) } @@ -161,6 +162,10 @@ where updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; let is_initiator = atomic_secret.is_none(); + if slate.multisig_key_id.is_none() { + return Err(ErrorKind::GenericError("missing multisig key id".into()).into()); + } + // Sender selects outputs into a new slate and save our corresponding keys in // a transaction context. The secret key in our transaction context will be // randomly selected. This returns the public slate, and a closure that locks @@ -168,6 +173,7 @@ where // according to plan // This function is just a big helper to do all of that, in theory // this process can be split up in any way + let multisig_key_id = slate.multisig_key_id.clone(); let mut context = selection::build_send_tx( wallet, &wallet.keychain(keychain_mask)?, @@ -180,6 +186,7 @@ where selection_strategy_is_use_all, None, parent_key_id.clone(), + multisig_key_id.as_ref(), use_test_rng, is_initiator, )?; @@ -233,6 +240,7 @@ where selection_strategy_is_use_all, None, parent_key_id.clone(), + None, use_test_rng, is_initiator, )?; @@ -394,6 +402,7 @@ where init_tx_args.num_change_outputs as usize, init_tx_args.selection_strategy_is_use_all, &parent_key_id, + None, )?; slate.fee_fields = FeeFields::new(0, fee)?; diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 663247085..3be7d705f 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -25,7 +25,8 @@ use crate::grin_core::core::transaction::{ use crate::grin_core::libtx::{aggsig, build, proof::ProofBuild, tx_fee}; use crate::grin_core::map_vec; use crate::grin_keychain::{ - BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Identifier, Keychain, SwitchCommitmentType, + BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Identifier, Keychain, + SwitchCommitmentType, }; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::Commitment; @@ -144,6 +145,8 @@ pub struct Slate { pub payment_proof: Option, /// Kernel features arguments pub kernel_features_args: Option, + /// Multisig key id for shared output + pub multisig_key_id: Option, } impl fmt::Display for Slate { @@ -342,6 +345,7 @@ impl Slate { }, payment_proof: None, kernel_features_args: None, + multisig_key_id: None, } } @@ -932,7 +936,7 @@ impl Slate { 4 => SlateVersion::V4, 5 | _ => SlateVersion::V5, } - } + } /// Calculate multisig key ID pub fn create_multisig_id(&self) -> Identifier { @@ -997,6 +1001,7 @@ impl From for SlateV4 { version_info, payment_proof, kernel_features_args, + multisig_key_id: _, } = slate.clone(); let participant_data = map_vec!(participant_data, |data| ParticipantDataV4::from(data)); let ver = VersionCompatInfoV4::from(&version_info); @@ -1044,6 +1049,7 @@ impl From for SlateV5 { version_info, payment_proof, kernel_features_args, + multisig_key_id, } = slate.clone(); let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); let ver = VersionCompatInfoV5::from(&version_info); @@ -1070,6 +1076,7 @@ impl From for SlateV5 { ver, proof: payment_proof, feat_args, + multisig_key_id, } } } @@ -1090,6 +1097,7 @@ impl From<&Slate> for SlateV4 { version_info, payment_proof, kernel_features_args, + multisig_key_id: _, } = slate; let num_parts = *num_parts; let id = *id; @@ -1143,6 +1151,7 @@ impl From<&Slate> for SlateV5 { version_info, payment_proof, kernel_features_args, + multisig_key_id, } = slate; let num_parts = *num_parts; let id = *id; @@ -1162,6 +1171,7 @@ impl From<&Slate> for SlateV5 { Some(a) => Some(KernelFeaturesArgsV5::from(a)), None => None, }; + let multisig_key_id = multisig_key_id.clone(); SlateV5 { num_parts, id, @@ -1176,6 +1186,7 @@ impl From<&Slate> for SlateV5 { ver, proof: payment_proof, feat_args, + multisig_key_id, } } } @@ -1466,6 +1477,7 @@ impl From for Slate { version_info, payment_proof, kernel_features_args, + multisig_key_id: None, } } } @@ -1487,6 +1499,7 @@ impl From for Slate { ver, proof: payment_proof, feat_args, + multisig_key_id, } = slate.clone(); let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); let version_info = VersionCompatInfo::from(&ver); @@ -1513,6 +1526,7 @@ impl From for Slate { version_info, payment_proof, kernel_features_args, + multisig_key_id, } } } diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs index 63e4b2678..81997390c 100644 --- a/libwallet/src/slate_versions/v5.rs +++ b/libwallet/src/slate_versions/v5.rs @@ -96,6 +96,10 @@ pub struct SlateV5 { #[serde(default = "default_kernel_features_none")] #[serde(skip_serializing_if = "Option::is_none")] pub feat_args: Option, + /// Multisig output identifier + #[serde(default = "default_multisig_id_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub multisig_key_id: Option, } fn default_payment_none() -> Option { @@ -130,6 +134,10 @@ fn default_kernel_features_none() -> Option { None } +fn default_multisig_id_none() -> Option { + None +} + /// Slate state definition #[derive(Serialize, Deserialize, Debug, Clone)] pub enum SlateStateV5 { diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs index 40efb35d8..814460aac 100644 --- a/libwallet/src/slate_versions/v5_bin.rs +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -17,7 +17,7 @@ use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; use crate::grin_core::ser as grin_ser; use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; -use crate::grin_keychain::BlindingFactor; +use crate::grin_keychain::{BlindingFactor, Identifier, IDENTIFIER_SIZE}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; @@ -523,6 +523,12 @@ impl Writeable for SlateV5Bin { }; writer.write_u64(lock_hgt)?; } + if let Some(mid) = v5.multisig_key_id.as_ref() { + writer.write_u8(1)?; + writer.write_fixed_bytes(mid.to_bytes())?; + } else { + writer.write_u8(0)?; + } Ok(()) } } @@ -549,6 +555,13 @@ impl Readable for SlateV5Bin { None }; + let multisig_key_id = if reader.read_u8()? != 0 { + let id_bytes = reader.read_fixed_bytes(IDENTIFIER_SIZE)?; + Some(Identifier::from_bytes(id_bytes.as_ref())) + } else { + None + }; + Ok(SlateV5Bin(SlateV5 { ver, id, @@ -563,6 +576,7 @@ impl Readable for SlateV5Bin { coms: opt_structs.coms, proof: opt_structs.proof, feat_args, + multisig_key_id, })) } } diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 1c9c97f37..0c3c61c01 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -242,7 +242,7 @@ where /// Return the current atomic secret index fn current_atomic_id(&mut self) -> Result; - /// Next atomic ID when we want to create a new atomic secret + /// Next atomic ID when we want to create a new atomic secret fn next_atomic_id(&mut self, keychain_mask: Option<&SecretKey>) -> Result; /// Get the atomic ID for the atomic swap associated with the given UUID @@ -743,7 +743,7 @@ impl Context { nonce: &PublicKey, ) -> Result { let mut common = nonce.clone(); - common.mul_assign(secp, &self.sec_key)?; + common.mul_assign(secp, &self.sec_nonce)?; let mut hasher = Sha3_256::new(); hasher.input(b"multisig_common_nonce"); hasher.input(&common.serialize_vec(secp, true)); diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 49c54335b..0bc008668 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -111,7 +111,7 @@ subcommands: help: EXPERIMENTAL - Do not lock the coins immediately, instead only lock them during finalization. short: l long: late-lock - - mutlisig: + - multisig: help: EXPERIMENTAL - Create a multisig output with shared ownership. long: multisig - change_outputs: @@ -280,6 +280,10 @@ subcommands: - send_atomic: about: Builds an atomic swap main transaction to send coins and sends to the recipient via the Slatepack workflow args: + - multisig_path: + help: BIP32 path of the multisig output to spend in the atomic swap + long: multisig_path + takes_value: true - refund: help: Create an atomic swap refund transaction short: r diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index bf8d313fa..d111c7940 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -469,7 +469,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Result Result Result<(), grin_wallet_controller::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // wallet init + let arg_vec = vec!["grin-wallet", "-p", "password1", "init", "-h"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + // trying to init twice - should fail + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).is_err()); + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + // add wallet to proxy + //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let wallet_config1 = config1.clone().members.unwrap().wallet; + + let (wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + client1.clone(), + "password1", + "default", + )?; + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1_i.clone(), + ); + + // Create wallet 2 + let arg_vec = vec!["grin-wallet", "-p", "password2", "init", "-h"]; + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let wallet_config2 = config2.clone().members.unwrap().wallet; + let (wallet2, mask2_i) = instantiate_wallet( + wallet_config2.clone(), + client2.clone(), + "password2", + "default", + )?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Create some accounts in wallet 1 + let arg_vec = vec!["grin-wallet", "-p", "password1", "account", "-c", "mining"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Create some accounts in wallet 2 + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + // Mine a bit into wallet 1 so we have something to send + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = + instantiate_wallet(wallet_config1, client1.clone(), "password1", "default")?; + let mask1 = (&mask1_i).as_ref(); + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + Ok(()) + }, + )?; + + // Mine a bit into wallet 2 so we have something to send + let wallet_config2 = config2.clone().members.unwrap().wallet; + let (wallet2, mask2_i) = + instantiate_wallet(wallet_config2, client2.clone(), "password2", "default")?; + let mask2 = (&mask2_i).as_ref(); + grin_wallet_controller::controller::owner_single_use( + Some(wallet2.clone()), + mask2, + None, + |api, m| { + api.set_active_account(m, "account_1")?; + Ok(()) + }, + )?; + + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet2.clone(), mask2, bh as usize, false); + + // Update info and check + let arg_vec = vec!["grin-wallet", "-p", "password1", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // create multisig output funding transaction + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "--multisig", + "5", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b00.M1.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b00.M2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "process_multisig", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b00.M3.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "account_1", + "-p", + "password2", + "finalize", + "-n", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b00.M4.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // mine some more coins to add confirmations to the multisig transaction + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-a", "account_1", "-p", "password2", "txs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // create atomic swap refund transaction + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send_atomic", + "-r", // create a refund transaction + "--multisig_path", + "m/2622924661/3526545887/2606926411/331176156", + "--min_conf", + "0", + "4.9875", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b01.A1.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive_atomic", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + // shouldn't be allowed to receive twice + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b01.A2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "countersign_atomic", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b01.A3.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "account_1", + "-p", + "password2", + "finalize_atomic", + "-n", // don't post the refund transaction + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // create atomic swap main transaction + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "send_atomic", + "--multisig_path", + "m/2622924661/3526545887/2606926411/331176156", + "--min_conf", + "0", + "4.9875", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + let arg_vec = vec!["grin-wallet", "-a", "account_1", "-p", "password2", "txs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b02.A1.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "receive_atomic", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + // shouldn't be allowed to receive twice + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec).is_err()); + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b02.A2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "countersign_atomic", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b02.A3.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize_atomic", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b02.A4.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "account_1", + "-p", + "password2", + "recover_atomic_secret", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-a", + "account_1", + "-p", + "password2", + "get_atomic_secrets", + "-i", + "1", // atomic ID + "--amount", + "4.9875", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn wallet_command_line() { + let test_dir = "target/test_output/command_line_atomic"; + if let Err(e) = command_line_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs index 784526a9f..bf97d4080 100644 --- a/tests/owner_v3_lifecycle.rs +++ b/tests/owner_v3_lifecycle.rs @@ -27,7 +27,7 @@ use std::thread; use std::time::Duration; use grin_wallet_impls::DefaultLCProvider; -use grin_wallet_libwallet::{InitTxArgs, Slate, SlateVersion, VersionedSlate}; +use grin_wallet_libwallet::{InitTxArgs, Slate, VersionedSlate}; use grin_wallet_util::grin_keychain::ExtKeychain; use serde_json;