Files
wiescheGitHubJoergardocratdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
90846a4212 Mwixnet wallet support (#783)
* Update rust edition to 2021 (#749)

* build: update rust edition 2021

* cargo: format all

* build: fix tests and warnings

* Save last scanned block info for wallet scanning (#748)

* scan: save last scanned block info for initial wallet scanning

* scan: save start height at scanned block info on outputs collecting

* scan: 10k blocks batches

* fix: include last height into batch on scan

* scan: save hash of last block

* scan: add larger window (2880 blocks) to scan outputs when updating wallet state

* add test batched scan and resume, add scan doc

* rename corrupted restore test

---------

Co-authored-by: Joerg <wiesche89@googlemail.com>

* Bump openssl from 0.10.68 to 0.10.80 (#752)

* Update lmdb (#755)

* wallet: update lmdb from store crate, migrate single backend from trait to struct, update tests cargo deps and design doc

* backend: do not panic on iter and directory creation

* return  iterator errors

* build: update to last node revision

* fix: height selection for scan, remove unused mutability for wallet

* wallet: remove unused mutability conversion

* tx: store slate state at database

* build: cargo fmt

* fix: do not return error when slate not found on tx slate state update

* lmdb: do not collect all tx log records at iterator, show read errors at log

* fix: save slate state, return error if slate not found

* tx: specify amount of bad txs on slate state update error

---------

Co-authored-by: Joerg <wiesche89@googlemail.com>

* Node submodules (#758)

* build: use node submodule as dependency

* ci: checkout submodules

* docs: add contributing and build instruction

* ci: update release build

* git: remove old submodule

* git: grin staging submodule

* git: add submodule dir

* Integrated Tor arti Client (#750)

* tor: integrated client for listener, update tokio to last version

* tor: send transaction with integrated client

* build: code format

* tor: optimize bootstrap percent check

* tor: handle config build error

* tor: do not show slatepack address if tor not launched

* tor: handle keystore errors

* tor: keep service launched

* tor: bootstrap timeout, clean cache before client launch

* tor: post request timeout

* fix: post timeout

* build: update deps after merge

* tor: store arti runtime

* tor: do not send over tor if `manual` arg presents or `skip_send_attempt` set at config

* fix: arg name

* tor: handle arti runtime creation error

* tor: handle connection error

* tor: handle bootstrap error

* tor: handle start service name error

* tor: handle service config creation error

* tor: service proxy errors

* tor: handle service key creation emptiness

* build: format code

* build: update arti to 0.43, hyper to 0.10.1

* api: start foreign listener before tor connection

* tor: handle json parse errors

* tor: prevent to create several client runtimes, ability to recreate

* tor: return bootstrap error

* tor: handle host parse error

* tor: restart proxy on error

* tor: do not use integrated client when config value use_integrated is missing, add logging

* build: update lock file

* Global Tor client state (#762)

* tor: global client state

* tor: optimize init client thread-safety

* tor: handle config change on client init

* tor: add logging for client reuse or reinit

* tor: do not reuse arti client for requests

* Optimize tor sync flow (#763)

* Connection timeout (#765)

* Update deps (#768)

* build: update grin submodule to last staging

* build: update grin submodule

* build: remove remove_dir_all dependency

* build: update rpassword

* build: update semver

* build: remove unused deps

* build: update ring

* build: update lock file deps

* build: update rustyline

* build: update ed25519-dalek

* build: update curve25519-dalek

* build: update age

* build: update uuid

* fix: uuid test

* build: update x25519-dalek

* fix: x25519-dalek build

* build: update arti

* build: update grin submodule

* build: update hyper

* fix: initialize rustls provider for tests

* build: update grin submodule

* fix: rustls install provider

* tor: share getting private key

* fix: show an error on node version parse

* cli: renamed prompt password method

* fix: replace into_boxed_slice with try_from

* fix: dalek tests for onion

* config: comment expanded secret key

* fix: import api body for controller

* password: handle prompt error

* wallet: parse node error message

* tests: do not use random values for sign messages

* build: make hook executable

* build: cargo format all

* rustls: do not install provider at tests and globally

* build: update grin submodule

* build: fix api channel from grin submodule

* args: use read_password for piped prompt

* build: remove unused dep

* test: fixed signature

* preserve piped pw input

---------

Co-authored-by: wiesche <wiesche89@googlemail.com>

* Bump lodash from 4.17.21 to 4.18.1 (#774)

* build: update lodash to 4.18.1

* fix: readme

* docs: add owner api authentication setup

* build: update package-lock.json to version 3

* fix: token result, inline some vars

---------

Co-authored-by: ardocrat <ardocrat@gri.mw>

* Global config instance (#769)

* config: global instance

* fix: cli tests

* fix: owner api tests

* config: save tor config

* fix: owner rpc tests

* fix: set config file path on init and owner api call

* tor: store launched services, get global config at command loop, remove unused listen args, check for empty global config to set default, do not save empty tor config at owner

* api: update config in memory after successful file write

* arti: restart proxy in loop

* fix: comment for tor config save

* config: set or init, update without error

* config: scoped tor config

* build: update lock file

* config: ability to store multiple configurations at memory from config path

* tor: restart service on config change

* fix: owner api test

* foreign: pass tor config from args, stop service, error handling

* arti: move runtime wrapper to single file

* config: read existing config from file at create_config

* config: handle error on read

* arti: single dir for cache

* foreign: register config listener before launch

* config: save temp

* config: remove unused error type

* config: make path and members not optional, return an error on config creation if it exists, pass default tor config to send if not set

* arti: non public runtime wrapper

* config: migrate comments

* fix: rpc tests

* config: return from cache if exists

* config: save disabled tor config when null provided at owner and tor section was not set

* config: update tor with args at listener

* config: pass pathbuf directly on creation

* config: unique tmp file name, setting permissions and providing config version

* fix global config handling

* fix config persistence

* fix tor service restart

* fix config synchronization

* require wallet close before changing data directory

* unify global config updates

---------

Co-authored-by: wiesche <wiesche89@googlemail.com>

* Wrong Slatepack address fix (#773)

* Use native tls for client (#779)

* client: use native tls and disable default features for reqwest

* build: bring back reqwest default features except tls

* client: remove tls selection

* expose mwixnet onion apis (#781)

* Update jayson and uuid (#777)

* build: update jayson and uuid

* npm: update lock file to v3

* npm: update lock file

---------

Co-authored-by: ardocrat <ardocrat@gri.mw>

* Optimize outputs scan and selection (#780)

* Make updater APIs public (#782)

* Add mwixnet wallet request support

* mwixnet key conversion tests

* validate and track mwixnet requests

* fix Arti HTTP POST request formatting

* add mwixnet wallet CLI submission

* unlock mwixnet outputs when requests are not sent

* support automatic mwixnet output selection

* move mwixnet response parsing to libwallet

* limit mwixnet hops

* centralize grin workspace dependencies

* fix tx filter

* handle unavailable terminal colors

* reject invalid invoice locking

* clarify bip32 derivation paths

* restore derivation path comments

* tor: downgrade arti to 0.43

* owner: put active account on top of list

* account table layout

* fix: some typos, imports, unwraps and formatting

* tx: lock outputs before sending with tor

* accounts: display balances at CLI and from owner API when min confirmations are set

* unpack plain slatepacks without password (#805)

* unpack plain slatepacks without password

* cli: better open wallet handling on wallet subcommand

---------

Co-authored-by: ardocrat <ardocrat@gri.mw>

* account: do not skip info from serialization, fix owner rpc test

* reject zero send amounts (#799)

* cli: fix short for min confirmations at accounts subcommand

* accounts: optimize accounts selection

* owner: document minimum_confirmations for accounts

* accounts: optimize selection

* preserve accounts api compatibi and clean up balances

* avoid empty wallet seeds (#801)

* fix empty wallet seeds

* mnemonic: return actual error with entropy size on recovering

* seed: extend mnemonic error description

* seed: clarify mnemonic errors

* seed: clarify mnemonic errors, fix missing phrase test

* Clean up seed handling add test

---------

Co-authored-by: ardocrat <ardocrat@gri.mw>

* owner: use same wallet lock for selecting and locking outputs on send

* fix: do not lock for late lock

* cli: explain how to cancel

* fix: error message for offline address and integrated tor

* test: late lock for cli and owner api

* wallet: persistence for active account

* fix: owner_rpc test

* fix: lock wallet till lock outputs on send

* fix: update docs when to call tx_lock_outputs

* build: update grin submodule

* tests: get rid of owner_single_use to use correct config path

* test: parallel send attempts

* fix: cancel hint

* fix: late lock test refresh balance

* fix: set active account at single batch

* owner: document persistence of set_active_account

* cli: error when create and active args are present at account

* fix: keep display account type as str

* restore windows test fixes

* fix locking docs cancel hint and tests

* test cli send failure

* fix current account flag

* fix active account use

* test account persistence

* fix cli account selection

* fix: cmd tests

* fix: cmd test

* fix account test

* Show HTTP status when node requests fail (#820)

* ci: cargo clippy

* fix submodule checkout

* update grin for clippy fixes

* isolate tests

* fix cli test

* fix clippy warnings

* fix mwixnet output lookup

* build: update grin submodule

* git: add clippy to hook

* fix review nits

* fix checks

* mwixnet: use MwixnetServerPublicKey wrapper for hop

* build: update grin submodule

* build: clippy fixes

* mwixnet: rename dalek public key

* simplify mwixnet keys

* clarify mwixnet docs

* clarify arti usage

* simplify mwixnet tests

* tidy mwixnet code and docs

* skip spent outputs

* simplify mwixnet send

* drop host clone

* fix mwixnet selection and arti errors

* make mwixnet fee optional

* mwixnet: use fee at amount format, show min on error

* fix: fee per hop test

* fix: fee error text check

* tor: increase circuit timeout to 60

* fix mwixnet refresh and help

* build: update grin submodule and mwixnet tests

* cli: do not update wallet state on cancellation of tx after failed mwixnet request

* mwixnet: create separate owner api method to send request, do not cancel tx on failed mwixnet request

* fix mwixnet cli args and error messages

---------

Co-authored-by: ardocrat <ardocrat@gri.mw>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 21:15:56 +02:00

325 lines
10 KiB
Rust

// Copyright 2024 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 a wallet sending to self, then creation of comsig request
#[macro_use]
extern crate log;
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
use grin_core as core;
use grin_core::core::FeeFields;
use grin_util as util;
use grin_util::secp::key::SecretKey;
use std::path::PathBuf;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self, LocalWalletClient};
use libwallet::{
mwixnet::{MixnetReqCreationParams, MwixnetServerPublicKey, MAX_MWIXNET_HOPS},
InitTxArgs, OutputStatus, TxLogEntryType,
};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
/// self send impl
fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// Create a new proxy to simulate server and wallet responses
let mut wallet_proxy = create_wallet_proxy(test_dir);
let chain = wallet_proxy.chain.clone();
let stopper = wallet_proxy.running.clone();
// Create a new wallet test client, and set its queues to communicate with the
// proxy
create_wallet_and_add!(
client1,
wallet1,
mask1_i,
test_dir,
"wallet1",
None,
&mut wallet_proxy,
true,
api1
);
let mask1 = mask1_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
api1.create_account_path(mask1, "mining")?;
api1.create_account_path(mask1, "listener")?;
// Get some mining done
{
wallet_inst!(wallet1, w);
w.set_account_by_name("mining")?;
}
let mut bh = 10u64;
let _ =
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, bh * reward);
// send to send
let args = InitTxArgs {
src_acct_name: Some("mining".to_owned()),
amount: reward * 2,
minimum_confirmations: 2,
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: true,
..Default::default()
};
let mut slate = api1.init_send_tx(mask1, args)?;
api1.tx_lock_outputs(mask1, &slate)?;
// Send directly to self
wallet::controller::foreign_single_use(
wallet1.clone(),
PathBuf::from(test_dir),
mask1_i.clone(),
|api| {
slate = api.receive_tx(&slate, Some("listener"), None)?;
Ok(())
},
)?;
slate = api1.finalize_tx(mask1, &slate)?;
api1.post_tx(mask1, &slate, false)?; // mines a block
bh += 1;
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
bh += 3;
// Check total in mining account
let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, bh * reward - reward * 2);
// Check total in 'listener' account
{
wallet_inst!(wallet1, w);
w.set_account_by_name("listener")?;
}
let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, 2 * reward);
// Recipient wallet creates a mwixnet request from the last output
let secp_locked = util::static_secp_instance();
let secp = secp_locked.lock();
let server_pubkey_str_1 = "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332";
let server_pubkey_str_2 = "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e";
let server_pubkey_str_3 = "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5";
let server_key_1 =
SecretKey::from_slice(&secp, &grin_util::from_hex(server_pubkey_str_1).unwrap())?;
let server_key_2 =
SecretKey::from_slice(&secp, &grin_util::from_hex(server_pubkey_str_2).unwrap())?;
let server_key_3 =
SecretKey::from_slice(&secp, &grin_util::from_hex(server_pubkey_str_3).unwrap())?;
let params = MixnetReqCreationParams {
server_keys: vec![
MwixnetServerPublicKey::from_secret(&server_key_1),
MwixnetServerPublicKey::from_secret(&server_key_2),
MwixnetServerPublicKey::from_secret(&server_key_3),
],
fee_per_hop: None,
};
let outputs = api1.retrieve_outputs(mask1, false, false, None)?;
// get last output
let last_output = outputs.1[outputs.1.len() - 1].clone();
// Store the output with an MMR index, like a restored output
let mmr_index = Some(chain.get_output_pos(&last_output.commit).unwrap());
{
wallet_inst!(wallet1, w);
let mut batch = w.batch(mask1)?;
let mut output = last_output.output.clone();
batch.delete(&output.key_id, &output.mmr_index)?;
output.mmr_index = mmr_index;
batch.save(output)?;
batch.commit()?;
}
let empty_params = MixnetReqCreationParams {
server_keys: vec![],
fee_per_hop: params.fee_per_hop,
};
assert!(api1
.create_mwixnet_req(mask1, &empty_params, &last_output.commit, true)
.is_err());
let too_many_params = MixnetReqCreationParams {
server_keys: vec![params.server_keys[0]; MAX_MWIXNET_HOPS + 1],
fee_per_hop: params.fee_per_hop,
};
assert!(api1
.create_mwixnet_req(mask1, &too_many_params, &last_output.commit, true)
.is_err());
let oversized_fee_params = MixnetReqCreationParams {
server_keys: params.server_keys[..2].to_vec(),
fee_per_hop: Some(1 << 39),
};
assert_eq!(
api1.create_mwixnet_req(mask1, &oversized_fee_params, &last_output.commit, true)
.unwrap_err(),
libwallet::Error::Fee("mwixnet total fee exceeds FeeFields limit".to_string())
);
for fee in [0, 12_499_999] {
let invalid_params = MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: Some(fee),
};
assert_eq!(
api1.create_mwixnet_req(mask1, &invalid_params, &last_output.commit, true)
.unwrap_err(),
libwallet::Error::Fee("mwixnet fee is below the swap minimum 0.0125".to_string())
);
}
let custom_params = MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: Some(25_000_000),
};
let custom = api1.create_mwixnet_req(mask1, &custom_params, &last_output.commit, false)?;
let mut onion = custom.request.onion;
for server_key in [&server_key_1, &server_key_2, &server_key_3] {
let peeled = onion.peel_layer(server_key).unwrap();
assert_eq!(peeled.payload.fee, FeeFields::try_from(25_000_000u64)?);
onion = peeled.onion;
}
drop(secp);
// Failed refresh must not create or lock a swap
let tx_count = api1.retrieve_txs(mask1, false, None, None, None)?.1.len();
let (disconnected, receiver) = std::sync::mpsc::channel();
drop(receiver);
let original_proxy = std::mem::replace(&mut *client1.proxy_tx.lock(), disconnected);
let mut api1 = api1;
let result = wallet::command::mwixnet(
&mut api1,
mask1,
wallet::command::MwixnetArgs {
server: "mza3u6vkqodqc6kjjapfbcjrg7hgpkuw6nyq5xvfjqp5jm2lfeaocwqd.onion".try_into()?,
output: wallet::command::MwixnetOutput::Commitment(last_output.commit),
minimum_confirmations: 1,
params: MixnetReqCreationParams {
server_keys: params.server_keys.clone(),
fee_per_hop: None,
},
},
);
*client1.proxy_tx.lock() = original_proxy;
assert!(result
.unwrap_err()
.to_string()
.contains("Could not refresh wallet outputs"));
assert_eq!(
api1.retrieve_txs(mask1, false, None, None, None)?.1.len(),
tx_count
);
let outputs = api1.retrieve_outputs(mask1, false, false, None)?.1;
assert_eq!(
outputs
.iter()
.find(|o| o.commit == last_output.commit)
.unwrap()
.output
.status,
OutputStatus::Unspent
);
let creation = api1.create_mwixnet_req(mask1, &params, &last_output.commit, true)?;
let creation_tx_id = creation.tx_id.unwrap();
let fee_per_hop = 12_500_000;
let mut onion = creation.request.onion.clone();
for server_key in [&server_key_1, &server_key_2, &server_key_3] {
let peeled = onion
.peel_layer(server_key)
.map_err(|e| libwallet::Error::GenericError(e.to_string()))?;
assert_eq!(peeled.payload.fee, FeeFields::try_from(fee_per_hop)?);
onion = peeled.onion;
}
println!("MWIXNET REQ: {:?}", creation.request);
// Check the input lock and expected output are tracked together.
let outputs = api1.retrieve_outputs(mask1, false, false, None)?;
let input = outputs
.1
.iter()
.find(|o| o.commit == last_output.commit)
.unwrap();
assert_eq!(input.output.status, OutputStatus::Locked);
assert_eq!(input.output.mmr_index, mmr_index);
let total_fee = fee_per_hop * params.server_keys.len() as u64;
let expected_amount = last_output.output.value - total_fee;
let expected_output = outputs
.1
.iter()
.find(|o| o.output.status == OutputStatus::Unconfirmed && o.output.value == expected_amount)
.unwrap();
assert_eq!(
input.output.tx_log_entry,
expected_output.output.tx_log_entry
);
let txs = api1.retrieve_txs(mask1, false, None, None, None)?.1;
let tx = txs.last().unwrap();
assert_eq!(creation_tx_id, tx.id);
assert_eq!(tx.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx.amount_debited, last_output.output.value);
assert_eq!(tx.amount_credited, expected_amount);
assert_eq!(tx.num_inputs, 1);
assert_eq!(tx.num_outputs, 1);
assert_eq!(tx.fee, Some(FeeFields::try_from(total_fee)?));
assert!(api1
.create_mwixnet_req(mask1, &params, &last_output.commit, false)
.is_err());
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(1000));
Ok(())
}
#[test]
fn mwixnet_comsig_test() {
let test_dir = "test_output/mwixnet";
setup(test_dir);
if let Err(e) = mwixnet_test_impl(test_dir) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
}