diff --git a/cmake/CheckTrezor.cmake b/cmake/CheckTrezor.cmake index ea21237fd..bcd3cfc2c 100644 --- a/cmake/CheckTrezor.cmake +++ b/cmake/CheckTrezor.cmake @@ -56,7 +56,7 @@ if(Protobuf_FOUND AND USE_DEVICE_TREZOR AND TREZOR_PYTHON) endif() if(USE_DEVICE_TREZOR_UDP_RELEASE) - add_definitions(-DWITH_DEVICE_TREZOR_UDP_RELEASE=1) + add_definitions(-DUSE_DEVICE_TREZOR_UDP_RELEASE=1) endif() if (Protobuf_INCLUDE_DIR) diff --git a/src/device/device.hpp b/src/device/device.hpp index dd9ad4332..399648f01 100644 --- a/src/device/device.hpp +++ b/src/device/device.hpp @@ -80,6 +80,14 @@ namespace hw { return false; } + class i_device_callback { + public: + virtual void on_button_request() {} + virtual void on_pin_request(epee::wipeable_string & pin) {} + virtual void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) {} + virtual ~i_device_callback() = default; + }; + class device { protected: std::string name; @@ -129,6 +137,8 @@ namespace hw { virtual device_type get_type() const = 0; virtual device_protocol_t device_protocol() const { return PROTOCOL_DEFAULT; }; + virtual void set_callback(i_device_callback * callback) {}; + virtual void set_derivation_path(const std::string &derivation_path) {}; /* ======================================================================= */ /* LOCKER */ diff --git a/src/device_trezor/device_trezor.cpp b/src/device_trezor/device_trezor.cpp index 5096fcea8..8868fb995 100644 --- a/src/device_trezor/device_trezor.cpp +++ b/src/device_trezor/device_trezor.cpp @@ -121,7 +121,8 @@ namespace trezor { const boost::optional & network_type){ AUTO_LOCK_CMD(); require_connected(); - test_ping(); + device_state_reset_unsafe(); + require_initialized(); auto req = std::make_shared(); this->set_msg_addr(req.get(), path, network_type); @@ -136,7 +137,8 @@ namespace trezor { const boost::optional & network_type){ AUTO_LOCK_CMD(); require_connected(); - test_ping(); + device_state_reset_unsafe(); + require_initialized(); auto req = std::make_shared(); this->set_msg_addr(req.get(), path, network_type); @@ -152,7 +154,8 @@ namespace trezor { { AUTO_LOCK_CMD(); require_connected(); - test_ping(); + device_state_reset_unsafe(); + require_initialized(); std::shared_ptr req; @@ -238,12 +241,11 @@ namespace trezor { cpend.construction_data = cdata.tx_data; // Transaction check - cryptonote::blobdata tx_blob; - cryptonote::transaction tx_deserialized; - bool r = cryptonote::t_serializable_object_to_blob(cpend.tx, tx_blob); - CHECK_AND_ASSERT_THROW_MES(r, "Transaction serialization failed"); - r = cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx_deserialized); - CHECK_AND_ASSERT_THROW_MES(r, "Transaction deserialization failed"); + try { + transaction_check(cdata, aux_data); + } catch(const std::exception &e){ + throw exc::ProtocolException(std::string("Transaction verification failed: ") + e.what()); + } std::string key_images; bool all_are_txin_to_key = std::all_of(cdata.tx.vin.begin(), cdata.tx.vin.end(), [&](const cryptonote::txin_v& s_e) -> bool @@ -283,7 +285,8 @@ namespace trezor { { AUTO_LOCK_CMD(); require_connected(); - test_ping(); + device_state_reset_unsafe(); + require_initialized(); CHECK_AND_ASSERT_THROW_MES(idx < unsigned_tx.txes.size(), "Invalid transaction index"); signer = std::make_shared(wallet, &unsigned_tx, idx, &aux_data); @@ -294,6 +297,7 @@ namespace trezor { // Step: Init auto init_msg = signer->step_init(); this->set_msg_addr(init_msg.get()); + transaction_pre_check(init_msg); auto response = this->client_exchange(init_msg); signer->step_init_ack(response); @@ -351,6 +355,59 @@ namespace trezor { signer->step_final_ack(ack_final); } + void device_trezor::transaction_pre_check(std::shared_ptr init_msg) + { + CHECK_AND_ASSERT_THROW_MES(init_msg, "TransactionInitRequest is empty"); + CHECK_AND_ASSERT_THROW_MES(init_msg->has_tsx_data(), "TransactionInitRequest has no transaction data"); + CHECK_AND_ASSERT_THROW_MES(m_features, "Device state not initialized"); // make sure the caller did not reset features + const bool nonce_required = init_msg->tsx_data().has_payment_id() && init_msg->tsx_data().payment_id().size() > 0; + + if (nonce_required){ + // Versions 2.0.9 and lower do not support payment ID + CHECK_AND_ASSERT_THROW_MES(m_features->has_major_version() && m_features->has_minor_version() && m_features->has_patch_version(), "Invalid Trezor firmware version information"); + const uint32_t vma = m_features->major_version(); + const uint32_t vmi = m_features->minor_version(); + const uint32_t vpa = m_features->patch_version(); + if (vma < 2 || (vma == 2 && vmi == 0 && vpa <= 9)) { + throw exc::TrezorException("Trezor firmware 2.0.9 and lower does not support transactions with short payment IDs or integrated addresses. Please update."); + } + } + } + + void device_trezor::transaction_check(const protocol::tx::TData & tdata, const hw::tx_aux_data & aux_data) + { + // Simple serialization check + cryptonote::blobdata tx_blob; + cryptonote::transaction tx_deserialized; + bool r = cryptonote::t_serializable_object_to_blob(tdata.tx, tx_blob); + CHECK_AND_ASSERT_THROW_MES(r, "Transaction serialization failed"); + r = cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx_deserialized); + CHECK_AND_ASSERT_THROW_MES(r, "Transaction deserialization failed"); + + // Extras check + std::vector tx_extra_fields; + cryptonote::tx_extra_nonce nonce; + + r = cryptonote::parse_tx_extra(tdata.tx.extra, tx_extra_fields); + CHECK_AND_ASSERT_THROW_MES(r, "tx.extra parsing failed"); + + const bool nonce_required = tdata.tsx_data.has_payment_id() && tdata.tsx_data.payment_id().size() > 0; + const bool has_nonce = cryptonote::find_tx_extra_field_by_type(tx_extra_fields, nonce); + CHECK_AND_ASSERT_THROW_MES(has_nonce == nonce_required, "Transaction nonce presence inconsistent"); + + if (nonce_required){ + const std::string & payment_id = tdata.tsx_data.payment_id(); + if (payment_id.size() == 32){ + crypto::hash payment_id_long{}; + CHECK_AND_ASSERT_THROW_MES(cryptonote::get_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_long), "Long payment ID not present"); + + } else if (payment_id.size() == 8){ + crypto::hash8 payment_id_short{}; + CHECK_AND_ASSERT_THROW_MES(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_short), "Short payment ID not present"); + } + } + } + #else //WITH_DEVICE_TREZOR void register_all(std::map> ®istry) { diff --git a/src/device_trezor/device_trezor.hpp b/src/device_trezor/device_trezor.hpp index a2134574c..1f08be887 100644 --- a/src/device_trezor/device_trezor.hpp +++ b/src/device_trezor/device_trezor.hpp @@ -57,9 +57,8 @@ namespace trezor { */ class device_trezor : public hw::trezor::device_trezor_base, public hw::device_cold { protected: - // To speed up blockchain parsing the view key maybe handle here. - crypto::secret_key viewkey; - bool has_view_key; + void transaction_pre_check(std::shared_ptr init_msg); + void transaction_check(const protocol::tx::TData & tdata, const hw::tx_aux_data & aux_data); public: device_trezor(); diff --git a/src/device_trezor/device_trezor_base.cpp b/src/device_trezor/device_trezor_base.cpp index 38c20c30b..5071932ee 100644 --- a/src/device_trezor/device_trezor_base.cpp +++ b/src/device_trezor/device_trezor_base.cpp @@ -28,6 +28,9 @@ // #include "device_trezor_base.hpp" +#include +#include +#include namespace hw { namespace trezor { @@ -36,10 +39,11 @@ namespace trezor { #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "device.trezor" +#define TREZOR_BIP44_HARDENED_ZERO 0x80000000 - const uint32_t device_trezor_base::DEFAULT_BIP44_PATH[] = {0x8000002c, 0x80000080, 0x80000000}; + const uint32_t device_trezor_base::DEFAULT_BIP44_PATH[] = {0x8000002c, 0x80000080}; - device_trezor_base::device_trezor_base() { + device_trezor_base::device_trezor_base(): m_callback(nullptr) { } @@ -61,7 +65,7 @@ namespace trezor { } bool device_trezor_base::set_name(const std::string & name) { - this->full_name = name; + this->m_full_name = name; this->name = ""; auto delim = name.find(':'); @@ -73,10 +77,10 @@ namespace trezor { } const std::string device_trezor_base::get_name() const { - if (this->full_name.empty()) { + if (this->m_full_name.empty()) { return std::string("name).append(">"); } - return this->full_name; + return this->m_full_name; } bool device_trezor_base::init() { @@ -135,6 +139,9 @@ namespace trezor { } bool device_trezor_base::disconnect() { + m_device_state.clear(); + m_features.reset(); + if (m_transport){ try { m_transport->close(); @@ -189,6 +196,25 @@ namespace trezor { } } + void device_trezor_base::require_initialized(){ + if (!m_features){ + throw exc::TrezorException("Device state not initialized"); + } + + if (m_features->has_bootloader_mode() && m_features->bootloader_mode()){ + throw exc::TrezorException("Device is in the bootloader mode"); + } + + if (m_features->has_firmware_present() && !m_features->firmware_present()){ + throw exc::TrezorException("Device has no firmware loaded"); + } + + // Hard requirement on initialized field, has to be there. + if (!m_features->has_initialized() || !m_features->initialized()){ + throw exc::TrezorException("Device is not initialized"); + } + } + void device_trezor_base::call_ping_unsafe(){ auto pingMsg = std::make_shared(); pingMsg->set_message("PING"); @@ -213,7 +239,7 @@ namespace trezor { void device_trezor_base::write_raw(const google::protobuf::Message * msg){ require_connected(); CHECK_AND_ASSERT_THROW_MES(msg, "Empty message"); - this->getTransport()->write(*msg); + this->get_transport()->write(*msg); } GenericMessage device_trezor_base::read_raw(){ @@ -221,7 +247,7 @@ namespace trezor { std::shared_ptr msg_resp; hw::trezor::messages::MessageType msg_resp_type; - this->getTransport()->read(msg_resp, &msg_resp_type); + this->get_transport()->read(msg_resp, &msg_resp_type); return GenericMessage(msg_resp_type, msg_resp); } @@ -252,6 +278,39 @@ namespace trezor { } } + void device_trezor_base::ensure_derivation_path() noexcept { + if (m_wallet_deriv_path.empty()){ + m_wallet_deriv_path.push_back(TREZOR_BIP44_HARDENED_ZERO); // default 0' + } + } + + void device_trezor_base::set_derivation_path(const std::string &deriv_path){ + this->m_wallet_deriv_path.clear(); + + if (deriv_path.empty() || deriv_path == "-"){ + ensure_derivation_path(); + return; + } + + CHECK_AND_ASSERT_THROW_MES(deriv_path.size() <= 255, "Derivation path is too long"); + + std::vector fields; + boost::split(fields, deriv_path, boost::is_any_of("/")); + CHECK_AND_ASSERT_THROW_MES(fields.size() <= 10, "Derivation path is too long"); + + boost::regex rgx("^([0-9]+)'?$"); + boost::cmatch match; + + this->m_wallet_deriv_path.reserve(fields.size()); + for(const std::string & cur : fields){ + const bool ok = boost::regex_match(cur.c_str(), match, rgx); + CHECK_AND_ASSERT_THROW_MES(ok, "Invalid wallet code: " << deriv_path << ". Invalid path element: " << cur); + CHECK_AND_ASSERT_THROW_MES(match[0].length() > 0, "Invalid wallet code: " << deriv_path << ". Invalid path element: " << cur); + + const unsigned long cidx = std::stoul(match[0].str()) | TREZOR_BIP44_HARDENED_ZERO; + this->m_wallet_deriv_path.push_back((unsigned int)cidx); + } + } /* ======================================================================= */ /* TREZOR PROTOCOL */ @@ -277,6 +336,25 @@ namespace trezor { return false; } + void device_trezor_base::device_state_reset_unsafe() + { + require_connected(); + auto initMsg = std::make_shared(); + + if(!m_device_state.empty()) { + initMsg->set_allocated_state(&m_device_state); + } + + m_features = this->client_exchange(initMsg); + initMsg->release_state(); + } + + void device_trezor_base::device_state_reset() + { + AUTO_LOCK_CMD(); + device_state_reset_unsafe(); + } + void device_trezor_base::on_button_request(GenericMessage & resp, const messages::common::ButtonRequest * msg) { CHECK_AND_ASSERT_THROW_MES(msg, "Empty message"); @@ -324,7 +402,13 @@ namespace trezor { // TODO: remove passphrase from memory m.set_passphrase(passphrase.data(), passphrase.size()); } + + if (!m_device_state.empty()){ + m.set_allocated_state(&m_device_state); + } + resp = call_raw(&m); + m.release_state(); } void device_trezor_base::on_passphrase_state_request(GenericMessage & resp, const messages::common::PassphraseStateRequest * msg) @@ -332,10 +416,7 @@ namespace trezor { MDEBUG("on_passhprase_state_request"); CHECK_AND_ASSERT_THROW_MES(msg, "Empty message"); - if (m_callback){ - m_callback->on_passphrase_state_request(msg->state()); - } - + m_device_state = msg->state(); messages::common::PassphraseStateAck m; resp = call_raw(&m); } diff --git a/src/device_trezor/device_trezor_base.hpp b/src/device_trezor/device_trezor_base.hpp index 88d419494..3c35e8aca 100644 --- a/src/device_trezor/device_trezor_base.hpp +++ b/src/device_trezor/device_trezor_base.hpp @@ -57,17 +57,6 @@ namespace trezor { #ifdef WITH_DEVICE_TREZOR class device_trezor_base; - /** - * Trezor device callbacks - */ - class trezor_callback { - public: - virtual void on_button_request() {}; - virtual void on_pin_request(epee::wipeable_string & pin) {}; - virtual void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) {}; - virtual void on_passphrase_state_request(const std::string & state) {}; - }; - /** * TREZOR device template with basic functions */ @@ -79,9 +68,12 @@ namespace trezor { mutable boost::mutex command_locker; std::shared_ptr m_transport; - std::shared_ptr m_callback; + i_device_callback * m_callback; - std::string full_name; + std::string m_full_name; + std::vector m_wallet_deriv_path; + std::string m_device_state; // returned after passphrase entry, session + std::shared_ptr m_features; // features from the last device reset cryptonote::network_type network_type; @@ -90,8 +82,11 @@ namespace trezor { // void require_connected(); + void require_initialized(); void call_ping_unsafe(); void test_ping(); + void device_state_reset_unsafe(); + void ensure_derivation_path() noexcept; // Communication methods @@ -139,7 +134,7 @@ namespace trezor { // Scoped session closer BOOST_SCOPE_EXIT_ALL(&, this) { if (open_session){ - this->getTransport()->close(); + this->get_transport()->close(); } }; @@ -187,9 +182,13 @@ namespace trezor { msg->add_address_n(x); } } else { + ensure_derivation_path(); for (unsigned int i : DEFAULT_BIP44_PATH) { msg->add_address_n(i); } + for (unsigned int i : m_wallet_deriv_path) { + msg->add_address_n(i); + } } if (network_type){ @@ -212,16 +211,26 @@ namespace trezor { bool reset(); // Default derivation path for Monero - static const uint32_t DEFAULT_BIP44_PATH[3]; + static const uint32_t DEFAULT_BIP44_PATH[2]; - std::shared_ptr getTransport(){ + std::shared_ptr get_transport(){ return m_transport; } - std::shared_ptr getCallback(){ + void set_callback(i_device_callback * callback) override { + m_callback = callback; + } + + i_device_callback * get_callback(){ return m_callback; } + std::shared_ptr & get_features() { + return m_features; + } + + void set_derivation_path(const std::string &deriv_path) override; + /* ======================================================================= */ /* SETUP/TEARDOWN */ /* ======================================================================= */ @@ -249,6 +258,11 @@ namespace trezor { */ bool ping(); + /** + * Performs Initialize call to the Trezor, resets to known state. + */ + void device_state_reset(); + // Protocol callbacks void on_button_request(GenericMessage & resp, const messages::common::ButtonRequest * msg); void on_pin_request(GenericMessage & resp, const messages::common::PinMatrixRequest * msg); diff --git a/src/device_trezor/trezor/tools/README.md b/src/device_trezor/trezor/tools/README.md index b176017ac..0802e734a 100644 --- a/src/device_trezor/trezor/tools/README.md +++ b/src/device_trezor/trezor/tools/README.md @@ -10,14 +10,28 @@ Install `protoc` for your distribution. Requirements: Soft requirement: Python 3, can be easily installed with [pyenv]. +If Python 3 is used there are no additional python dependencies. -### Python 2 +Since Cmake 3.12 the `FindPython` module is used to locate the Python +interpreter in your system. It preferably searches for Python 3, if none +is found, it searches for Python 2. -Workaround if there is no Python3 available: +Lower version of the cmake uses another module which does not guarantee +ordering. If you want to override the selected python you can do it in +the following way: ```bash -pip install backports.tempfile -``` +export TREZOR_PYTHON=`which python3` +``` + + +### Python 2.7+ + +Python 3 has `tempfile.TemporaryDirectory` available but Python 2 lacks +this class so the message generation code uses `backports.tempfile` package +bundled in the repository. + +The minimal Python versions are 2.7 and 3.4 ### Regenerate messages diff --git a/src/device_trezor/trezor/tools/pb2cpp.py b/src/device_trezor/trezor/tools/pb2cpp.py index 4d7cc775f..3e0318ea5 100644 --- a/src/device_trezor/trezor/tools/pb2cpp.py +++ b/src/device_trezor/trezor/tools/pb2cpp.py @@ -14,12 +14,18 @@ import hashlib try: from tempfile import TemporaryDirectory except: - # Py2 backward compatibility, optionally installed by user - # pip install backports.tempfile + # Py2 backward compatibility, using bundled sources. + # Original source: pip install backports.tempfile try: - from backports.tempfile import TemporaryDirectory + # Try bundled python version + import sys + sys.path.append(os.path.dirname(__file__)) + from py2backports.tempfile import TemporaryDirectory + except: - raise EnvironmentError('TemporaryDirectory could not be imported. Try: pip install backports.tempfile') + raise EnvironmentError('Python 2.7+ or 3.4+ is required. ' + 'TemporaryDirectory is not available in Python 2.' + 'Try to specify python to use, e.g.: "export TREZOR_PYTHON=`which python3`"') AUTO_HEADER = "# Automatically generated by pb2cpp\n" diff --git a/src/device_trezor/trezor/tools/py2backports/__init__.py b/src/device_trezor/trezor/tools/py2backports/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/device_trezor/trezor/tools/py2backports/tempfile.py b/src/device_trezor/trezor/tools/py2backports/tempfile.py new file mode 100644 index 000000000..e259dea3f --- /dev/null +++ b/src/device_trezor/trezor/tools/py2backports/tempfile.py @@ -0,0 +1,72 @@ +""" +https://github.com/pjdelport/backports.tempfile/blob/master/src/backports/tempfile.py +Partial backport of Python 3.5's tempfile module: + TemporaryDirectory +Backport modifications are marked with marked with "XXX backport". +""" +from __future__ import absolute_import + +import sys +import warnings as _warnings +from shutil import rmtree as _rmtree + +from py2backports.weakref import finalize + + +# XXX backport: Rather than backporting all of mkdtemp(), we just create a +# thin wrapper implementing its Python 3.5 signature. +if sys.version_info < (3, 5): + from tempfile import mkdtemp as old_mkdtemp + + def mkdtemp(suffix=None, prefix=None, dir=None): + """ + Wrap `tempfile.mkdtemp()` to make the suffix and prefix optional (like Python 3.5). + """ + kwargs = {k: v for (k, v) in + dict(suffix=suffix, prefix=prefix, dir=dir).items() + if v is not None} + return old_mkdtemp(**kwargs) + +else: + from tempfile import mkdtemp + + +# XXX backport: ResourceWarning was added in Python 3.2. +# For earlier versions, fall back to RuntimeWarning instead. +_ResourceWarning = RuntimeWarning if sys.version_info < (3, 2) else ResourceWarning + + +class TemporaryDirectory(object): + """Create and return a temporary directory. This has the same + behavior as mkdtemp but can be used as a context manager. For + example: + with TemporaryDirectory() as tmpdir: + ... + Upon exiting the context, the directory and everything contained + in it are removed. + """ + + def __init__(self, suffix=None, prefix=None, dir=None): + self.name = mkdtemp(suffix, prefix, dir) + self._finalizer = finalize( + self, self._cleanup, self.name, + warn_message="Implicitly cleaning up {!r}".format(self)) + + @classmethod + def _cleanup(cls, name, warn_message): + _rmtree(name) + _warnings.warn(warn_message, _ResourceWarning) + + + def __repr__(self): + return "<{} {!r}>".format(self.__class__.__name__, self.name) + + def __enter__(self): + return self.name + + def __exit__(self, exc, value, tb): + self.cleanup() + + def cleanup(self): + if self._finalizer.detach(): + _rmtree(self.name) diff --git a/src/device_trezor/trezor/tools/py2backports/weakref.py b/src/device_trezor/trezor/tools/py2backports/weakref.py new file mode 100644 index 000000000..eb646812b --- /dev/null +++ b/src/device_trezor/trezor/tools/py2backports/weakref.py @@ -0,0 +1,148 @@ +""" +https://github.com/pjdelport/backports.weakref/blob/master/src/backports/weakref.py +Partial backport of Python 3.6's weakref module: + finalize (new in Python 3.4) +Backport modifications are marked with "XXX backport". +""" +from __future__ import absolute_import + +import itertools +import sys +from weakref import ref + +__all__ = ['finalize'] + + +class finalize(object): + """Class for finalization of weakrefable objects + finalize(obj, func, *args, **kwargs) returns a callable finalizer + object which will be called when obj is garbage collected. The + first time the finalizer is called it evaluates func(*arg, **kwargs) + and returns the result. After this the finalizer is dead, and + calling it just returns None. + When the program exits any remaining finalizers for which the + atexit attribute is true will be run in reverse order of creation. + By default atexit is true. + """ + + # Finalizer objects don't have any state of their own. They are + # just used as keys to lookup _Info objects in the registry. This + # ensures that they cannot be part of a ref-cycle. + + __slots__ = () + _registry = {} + _shutdown = False + _index_iter = itertools.count() + _dirty = False + _registered_with_atexit = False + + class _Info(object): + __slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index") + + def __init__(self, obj, func, *args, **kwargs): + if not self._registered_with_atexit: + # We may register the exit function more than once because + # of a thread race, but that is harmless + import atexit + atexit.register(self._exitfunc) + finalize._registered_with_atexit = True + info = self._Info() + info.weakref = ref(obj, self) + info.func = func + info.args = args + info.kwargs = kwargs or None + info.atexit = True + info.index = next(self._index_iter) + self._registry[self] = info + finalize._dirty = True + + def __call__(self, _=None): + """If alive then mark as dead and return func(*args, **kwargs); + otherwise return None""" + info = self._registry.pop(self, None) + if info and not self._shutdown: + return info.func(*info.args, **(info.kwargs or {})) + + def detach(self): + """If alive then mark as dead and return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None and self._registry.pop(self, None): + return (obj, info.func, info.args, info.kwargs or {}) + + def peek(self): + """If alive then return (obj, func, args, kwargs); + otherwise return None""" + info = self._registry.get(self) + obj = info and info.weakref() + if obj is not None: + return (obj, info.func, info.args, info.kwargs or {}) + + @property + def alive(self): + """Whether finalizer is alive""" + return self in self._registry + + @property + def atexit(self): + """Whether finalizer should be called at exit""" + info = self._registry.get(self) + return bool(info) and info.atexit + + @atexit.setter + def atexit(self, value): + info = self._registry.get(self) + if info: + info.atexit = bool(value) + + def __repr__(self): + info = self._registry.get(self) + obj = info and info.weakref() + if obj is None: + return '<%s object at %#x; dead>' % (type(self).__name__, id(self)) + else: + return '<%s object at %#x; for %r at %#x>' % \ + (type(self).__name__, id(self), type(obj).__name__, id(obj)) + + @classmethod + def _select_for_exit(cls): + # Return live finalizers marked for exit, oldest first + L = [(f,i) for (f,i) in cls._registry.items() if i.atexit] + L.sort(key=lambda item:item[1].index) + return [f for (f,i) in L] + + @classmethod + def _exitfunc(cls): + # At shutdown invoke finalizers for which atexit is true. + # This is called once all other non-daemonic threads have been + # joined. + reenable_gc = False + try: + if cls._registry: + import gc + if gc.isenabled(): + reenable_gc = True + gc.disable() + pending = None + while True: + if pending is None or finalize._dirty: + pending = cls._select_for_exit() + finalize._dirty = False + if not pending: + break + f = pending.pop() + try: + # gc is disabled, so (assuming no daemonic + # threads) the following is the only line in + # this function which might trigger creation + # of a new finalizer + f() + except Exception: + sys.excepthook(*sys.exc_info()) + assert f not in cls._registry + finally: + # prevent any more finalizers from executing during shutdown + finalize._shutdown = True + if reenable_gc: + gc.enable() diff --git a/src/device_trezor/trezor/transport.cpp b/src/device_trezor/trezor/transport.cpp index 814537eb6..cd66e59e8 100644 --- a/src/device_trezor/trezor/transport.cpp +++ b/src/device_trezor/trezor/transport.cpp @@ -840,7 +840,7 @@ namespace trezor{ throw exc::DeviceAcquireException("Unable to claim libusb device"); } - m_conn_count += 1; + m_conn_count = 1; m_proto->session_begin(*this); #undef TREZOR_DESTROY_SESSION diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index bcf6acbd5..e57565015 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -3790,6 +3790,7 @@ boost::optional simple_wallet::new_wallet(const boost::pr { auto rc = tools::wallet2::make_new(vm, false, password_prompter); m_wallet = std::move(rc.first); + m_wallet->callback(this); if (!m_wallet) { return {}; @@ -3807,9 +3808,11 @@ boost::optional simple_wallet::new_wallet(const boost::pr m_wallet->set_refresh_from_block_height(m_restore_height); auto device_desc = tools::wallet2::device_name_option(vm); + auto device_derivation_path = tools::wallet2::device_derivation_path_option(vm); try { bool create_address_file = command_line::get_arg(vm, arg_create_address_file); + m_wallet->device_derivation_path(device_derivation_path); m_wallet->restore(m_wallet_file, std::move(rc.second).password(), device_desc.empty() ? "Ledger" : device_desc, create_address_file); message_writer(console_color_white, true) << tr("Generated new wallet on hw device: ") << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); @@ -3897,7 +3900,7 @@ bool simple_wallet::open_wallet(const boost::program_options::variables_map& vm) epee::wipeable_string password; try { - auto rc = tools::wallet2::make_from_file(vm, false, m_wallet_file, password_prompter); + auto rc = tools::wallet2::make_from_file(vm, false, "", password_prompter); m_wallet = std::move(rc.first); password = std::move(std::move(rc.second).password()); if (!m_wallet) @@ -3905,6 +3908,8 @@ bool simple_wallet::open_wallet(const boost::program_options::variables_map& vm) return false; } + m_wallet->callback(this); + m_wallet->load(m_wallet_file, password); std::string prefix; bool ready; uint32_t threshold, total; @@ -4308,6 +4313,61 @@ boost::optional simple_wallet::on_get_password(const char return pwd_container->password(); } //---------------------------------------------------------------------------------------------------- +void simple_wallet::on_button_request() +{ + message_writer(console_color_white, false) << tr("Device requires attention"); +} +//---------------------------------------------------------------------------------------------------- +void simple_wallet::on_pin_request(epee::wipeable_string & pin) +{ +#ifdef HAVE_READLINE + rdln::suspend_readline pause_readline; +#endif + std::string msg = tr("Enter device PIN"); + auto pwd_container = tools::password_container::prompt(false, msg.c_str()); + THROW_WALLET_EXCEPTION_IF(!pwd_container, tools::error::password_entry_failed, tr("Failed to read device PIN")); + pin = pwd_container->password(); +} +//---------------------------------------------------------------------------------------------------- +void simple_wallet::on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) +{ + if (on_device){ + message_writer(console_color_white, true) << tr("Please enter the device passphrase on the device"); + return; + } + +#ifdef HAVE_READLINE + rdln::suspend_readline pause_readline; +#endif + std::string msg = tr("Enter device passphrase"); + auto pwd_container = tools::password_container::prompt(false, msg.c_str()); + THROW_WALLET_EXCEPTION_IF(!pwd_container, tools::error::password_entry_failed, tr("Failed to read device passphrase")); + passphrase = pwd_container->password(); +} +//---------------------------------------------------------------------------------------------------- +void simple_wallet::on_refresh_finished(uint64_t start_height, uint64_t fetched_blocks, bool is_init, bool received_money) +{ + // Key image sync after the first refresh + if (!m_wallet->get_account().get_device().has_tx_cold_sign()) { + return; + } + + if (!received_money || m_wallet->get_device_last_key_image_sync() != 0) { + return; + } + + // Finished first refresh for HW device and money received -> KI sync + message_writer() << "\n" << tr("The first refresh has finished for the HW-based wallet with received money. hw_key_images_sync is needed. "); + + std::string accepted = input_line(tr("Do you want to do it now? (Y/Yes/N/No): ")); + if (std::cin.eof() || !command_line::is_yes(accepted)) { + message_writer(console_color_red, false) << tr("hw_key_images_sync skipped. Run command manually before a transfer."); + return; + } + + key_images_sync_intern(); +} +//---------------------------------------------------------------------------------------------------- bool simple_wallet::refresh_main(uint64_t start_height, enum ResetType reset, bool is_init) { if (!try_connect_to_daemon(is_init)) @@ -4325,13 +4385,14 @@ bool simple_wallet::refresh_main(uint64_t start_height, enum ResetType reset, bo message_writer() << tr("Starting refresh..."); uint64_t fetched_blocks = 0; + bool received_money = false; bool ok = false; std::ostringstream ss; try { m_in_manual_refresh.store(true, std::memory_order_relaxed); epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&](){m_in_manual_refresh.store(false, std::memory_order_relaxed);}); - m_wallet->refresh(m_wallet->is_trusted_daemon(), start_height, fetched_blocks); + m_wallet->refresh(m_wallet->is_trusted_daemon(), start_height, fetched_blocks, received_money); ok = true; // Clear line "Height xxx of xxx" std::cout << "\r \r"; @@ -4339,6 +4400,7 @@ bool simple_wallet::refresh_main(uint64_t start_height, enum ResetType reset, bo if (is_init) print_accounts(); show_balance_unlocked(); + on_refresh_finished(start_height, fetched_blocks, is_init, received_money); } catch (const tools::error::daemon_busy&) { @@ -8096,13 +8158,13 @@ bool simple_wallet::hw_key_images_sync(const std::vector &args) fail_msg_writer() << tr("hw wallet does not support cold KI sync"); return true; } - if (!m_wallet->is_trusted_daemon()) - { - fail_msg_writer() << tr("this command requires a trusted daemon. Enable with --trusted-daemon"); - return true; - } LOCK_IDLE_SCOPE(); + key_images_sync_intern(); + return true; +} +//---------------------------------------------------------------------------------------------------- +void simple_wallet::key_images_sync_intern(){ try { message_writer(console_color_white, false) << tr("Please confirm the key image sync on the device"); @@ -8111,19 +8173,23 @@ bool simple_wallet::hw_key_images_sync(const std::vector &args) uint64_t height = m_wallet->cold_key_image_sync(spent, unspent); if (height > 0) { - success_msg_writer() << tr("Signed key images imported to height ") << height << ", " - << print_money(spent) << tr(" spent, ") << print_money(unspent) << tr(" unspent"); - } else { + success_msg_writer() << tr("Key images synchronized to height ") << height; + if (!m_wallet->is_trusted_daemon()) + { + message_writer() << tr("Running untrusted daemon, cannot determine which transaction output is spent. Use a trusted daemon with --trusted-daemon and run rescan_spent"); + } else + { + success_msg_writer() << print_money(spent) << tr(" spent, ") << print_money(unspent) << tr(" unspent"); + } + } + else { fail_msg_writer() << tr("Failed to import key images"); } } catch (const std::exception &e) { fail_msg_writer() << tr("Failed to import key images: ") << e.what(); - return true; } - - return true; } //---------------------------------------------------------------------------------------------------- bool simple_wallet::hw_reconnect(const std::vector &args) diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 421afbeda..665879cac 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -241,6 +241,8 @@ namespace cryptonote bool print_ring_members(const std::vector& ptx_vector, std::ostream& ostr); std::string get_prompt() const; bool print_seed(bool encrypted); + void key_images_sync_intern(); + void on_refresh_finished(uint64_t start_height, uint64_t fetched_blocks, bool is_init, bool received_money); struct transfer_view { @@ -287,6 +289,9 @@ namespace cryptonote virtual void on_money_spent(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& in_tx, uint64_t amount, const cryptonote::transaction& spend_tx, const cryptonote::subaddress_index& subaddr_index); virtual void on_skip_transaction(uint64_t height, const crypto::hash &txid, const cryptonote::transaction& tx); virtual boost::optional on_get_password(const char *reason); + virtual void on_button_request(); + virtual void on_pin_request(epee::wipeable_string & pin); + virtual void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase); //---------------------------------------------------------- friend class refresh_progress_reporter_t; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index bbb801716..097278961 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -222,6 +222,7 @@ struct options { }; const command_line::arg_descriptor kdf_rounds = {"kdf-rounds", tools::wallet2::tr("Number of rounds for the key derivation function"), 1}; const command_line::arg_descriptor hw_device = {"hw-device", tools::wallet2::tr("HW device to use"), ""}; + const command_line::arg_descriptor hw_device_derivation_path = {"hw-device-deriv-path", tools::wallet2::tr("HW device wallet derivation path (e.g., SLIP-10)"), ""}; const command_line::arg_descriptor tx_notify = { "tx-notify" , "Run a program for each new incoming transaction, '%s' will be replaced by the transaction hash" , "" }; }; @@ -274,6 +275,7 @@ std::unique_ptr make_basic(const boost::program_options::variabl auto daemon_host = command_line::get_arg(vm, opts.daemon_host); auto daemon_port = command_line::get_arg(vm, opts.daemon_port); auto device_name = command_line::get_arg(vm, opts.hw_device); + auto device_derivation_path = command_line::get_arg(vm, opts.hw_device_derivation_path); THROW_WALLET_EXCEPTION_IF(!daemon_address.empty() && !daemon_host.empty() && 0 != daemon_port, tools::error::wallet_internal_error, tools::wallet2::tr("can't specify daemon host or port more than once")); @@ -329,6 +331,7 @@ std::unique_ptr make_basic(const boost::program_options::variabl boost::filesystem::path ringdb_path = command_line::get_arg(vm, opts.shared_ringdb_dir); wallet->set_ring_database(ringdb_path.string()); wallet->device_name(device_name); + wallet->device_derivation_path(device_derivation_path); try { @@ -838,6 +841,24 @@ wallet_keys_unlocker::~wallet_keys_unlocker() } } +void wallet_device_callback::on_button_request() +{ + if (wallet) + wallet->on_button_request(); +} + +void wallet_device_callback::on_pin_request(epee::wipeable_string & pin) +{ + if (wallet) + wallet->on_pin_request(pin); +} + +void wallet_device_callback::on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) +{ + if (wallet) + wallet->on_passphrase_request(on_device, passphrase); +} + wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended): m_multisig_rescan_info(NULL), m_multisig_rescan_k(NULL), @@ -891,7 +912,8 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended): m_ringdb(), m_last_block_reward(0), m_encrypt_keys_after_refresh(boost::none), - m_unattended(unattended) + m_unattended(unattended), + m_device_last_key_image_sync(0) { } @@ -914,6 +936,11 @@ std::string wallet2::device_name_option(const boost::program_options::variables_ return command_line::get_arg(vm, options().hw_device); } +std::string wallet2::device_derivation_path_option(const boost::program_options::variables_map &vm) +{ + return command_line::get_arg(vm, options().hw_device_derivation_path); +} + void wallet2::init_options(boost::program_options::options_description& desc_params) { const options opts{}; @@ -930,6 +957,7 @@ void wallet2::init_options(boost::program_options::options_description& desc_par command_line::add_arg(desc_params, opts.shared_ringdb_dir); command_line::add_arg(desc_params, opts.kdf_rounds); command_line::add_arg(desc_params, opts.hw_device); + command_line::add_arg(desc_params, opts.hw_device_derivation_path); command_line::add_arg(desc_params, opts.tx_notify); } @@ -949,7 +977,7 @@ std::pair, password_container> wallet2::make_from_file( return {nullptr, password_container{}}; } auto wallet = make_basic(vm, unattended, opts, password_prompter); - if (wallet) + if (wallet && !wallet_file.empty()) { wallet->load(wallet_file, pwd->password()); } @@ -1091,15 +1119,17 @@ bool wallet2::reconnect_device() hw::device &hwdev = lookup_device(m_device_name); hwdev.set_name(m_device_name); hwdev.set_network_type(m_nettype); + hwdev.set_derivation_path(m_device_derivation_path); + hwdev.set_callback(get_device_callback()); r = hwdev.init(); if (!r){ - LOG_PRINT_L2("Could not init device"); + MERROR("Could not init device"); return false; } r = hwdev.connect(); if (!r){ - LOG_PRINT_L2("Could not connect to the device"); + MERROR("Could not connect to the device"); return false; } @@ -2998,6 +3028,7 @@ bool wallet2::clear() m_subaddresses.clear(); m_subaddress_labels.clear(); m_multisig_rounds_passed = 0; + m_device_last_key_image_sync = 0; return true; } @@ -3159,6 +3190,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable value.SetString(m_device_name.c_str(), m_device_name.size()); json.AddMember("device_name", value, json.GetAllocator()); + value.SetString(m_device_derivation_path.c_str(), m_device_derivation_path.size()); + json.AddMember("device_derivation_path", value, json.GetAllocator()); + // Serialize the JSON object rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -3278,6 +3312,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_subaddress_lookahead_major = SUBADDRESS_LOOKAHEAD_MAJOR; m_subaddress_lookahead_minor = SUBADDRESS_LOOKAHEAD_MINOR; m_device_name = ""; + m_device_derivation_path = ""; m_key_device_type = hw::device::device_type::SOFTWARE; encrypted_secret_keys = false; } @@ -3445,6 +3480,9 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_device_name = m_key_device_type == hw::device::device_type::LEDGER ? "Ledger" : "default"; } } + + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, device_derivation_path, std::string, String, false, std::string()); + m_device_derivation_path = field_device_derivation_path; } else { @@ -3459,6 +3497,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ hw::device &hwdev = lookup_device(m_device_name); THROW_WALLET_EXCEPTION_IF(!hwdev.set_name(m_device_name), error::wallet_internal_error, "Could not set device name " + m_device_name); hwdev.set_network_type(m_nettype); + hwdev.set_derivation_path(m_device_derivation_path); + hwdev.set_callback(get_device_callback()); THROW_WALLET_EXCEPTION_IF(!hwdev.init(), error::wallet_internal_error, "Could not initialize the device " + m_device_name); THROW_WALLET_EXCEPTION_IF(!hwdev.connect(), error::wallet_internal_error, "Could not connect to the device " + m_device_name); m_account.set_device(hwdev); @@ -3965,6 +4005,8 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p auto &hwdev = lookup_device(device_name); hwdev.set_name(device_name); hwdev.set_network_type(m_nettype); + hwdev.set_derivation_path(m_device_derivation_path); + hwdev.set_callback(get_device_callback()); m_account.create_from_device(hwdev); m_key_device_type = m_account.get_device().get_type(); @@ -9238,9 +9280,7 @@ void wallet2::cold_sign_tx(const std::vector& ptx_vector, signed_tx_ //---------------------------------------------------------------------------------------------------- uint64_t wallet2::cold_key_image_sync(uint64_t &spent, uint64_t &unspent) { auto & hwdev = get_account().get_device(); - if (!hwdev.has_ki_cold_sync()){ - throw std::invalid_argument("Device does not support cold ki sync protocol"); - } + CHECK_AND_ASSERT_THROW_MES(hwdev.has_ki_cold_sync(), "Device does not support cold ki sync protocol"); auto dev_cold = dynamic_cast<::hw::device_cold*>(&hwdev); CHECK_AND_ASSERT_THROW_MES(dev_cold, "Device does not implement cold signing interface"); @@ -9251,7 +9291,11 @@ uint64_t wallet2::cold_key_image_sync(uint64_t &spent, uint64_t &unspent) { dev_cold->ki_sync(&wallet_shim, m_transfers, ski); - return import_key_images(ski, 0, spent, unspent); + // Call COMMAND_RPC_IS_KEY_IMAGE_SPENT only if daemon is trusted. + uint64_t import_res = import_key_images(ski, 0, spent, unspent, is_trusted_daemon()); + m_device_last_key_image_sync = time(NULL); + + return import_res; } //---------------------------------------------------------------------------------------------------- void wallet2::get_hard_fork_info(uint8_t version, uint64_t &earliest_height) const @@ -11985,4 +12029,29 @@ uint64_t wallet2::get_segregation_fork_height() const void wallet2::generate_genesis(cryptonote::block& b) const { cryptonote::generate_genesis_block(b, get_config(m_nettype).GENESIS_TX, get_config(m_nettype).GENESIS_NONCE); } +//---------------------------------------------------------------------------------------------------- +wallet_device_callback * wallet2::get_device_callback() +{ + if (!m_device_callback){ + m_device_callback.reset(new wallet_device_callback(this)); + } + return m_device_callback.get(); +}//---------------------------------------------------------------------------------------------------- +void wallet2::on_button_request() +{ + if (0 != m_callback) + m_callback->on_button_request(); +} +//---------------------------------------------------------------------------------------------------- +void wallet2::on_pin_request(epee::wipeable_string & pin) +{ + if (0 != m_callback) + m_callback->on_pin_request(pin); +} +//---------------------------------------------------------------------------------------------------- +void wallet2::on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) +{ + if (0 != m_callback) + m_callback->on_passphrase_request(on_device, passphrase); +} } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index d07385e96..91c68bf3c 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -100,11 +100,26 @@ namespace tools virtual void on_lw_money_received(uint64_t height, const crypto::hash &txid, uint64_t amount) {} virtual void on_lw_unconfirmed_money_received(uint64_t height, const crypto::hash &txid, uint64_t amount) {} virtual void on_lw_money_spent(uint64_t height, const crypto::hash &txid, uint64_t amount) {} + // Device callbacks + virtual void on_button_request() {} + virtual void on_pin_request(epee::wipeable_string & pin) {} + virtual void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) {} // Common callbacks virtual void on_pool_tx_removed(const crypto::hash &txid) {} virtual ~i_wallet2_callback() {} }; + class wallet_device_callback : public hw::i_device_callback + { + public: + wallet_device_callback(wallet2 * wallet): wallet(wallet) {}; + void on_button_request() override; + void on_pin_request(epee::wipeable_string & pin) override; + void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase) override; + private: + wallet2 * wallet; + }; + struct tx_dust_policy { uint64_t dust_threshold; @@ -156,6 +171,7 @@ namespace tools { friend class ::Serialization_portability_wallet_Test; friend class wallet_keys_unlocker; + friend class wallet_device_callback; public: static constexpr const std::chrono::seconds rpc_timeout = std::chrono::minutes(3) + std::chrono::seconds(30); @@ -177,6 +193,7 @@ namespace tools static bool has_testnet_option(const boost::program_options::variables_map& vm); static bool has_stagenet_option(const boost::program_options::variables_map& vm); static std::string device_name_option(const boost::program_options::variables_map& vm); + static std::string device_derivation_path_option(const boost::program_options::variables_map &vm); static void init_options(boost::program_options::options_description& desc_params); //! Uses stdin and stdout. Returns a wallet2 if no errors. @@ -795,6 +812,7 @@ namespace tools bool is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height) const; uint64_t get_last_block_reward() const { return m_last_block_reward; } + uint64_t get_device_last_key_image_sync() const { return m_device_last_key_image_sync; } template inline void serialize(t_archive &a, const unsigned int ver) @@ -903,6 +921,9 @@ namespace tools if(ver < 26) return; a & m_tx_device; + if(ver < 27) + return; + a & m_device_last_key_image_sync; } /*! @@ -964,6 +985,8 @@ namespace tools void confirm_non_default_ring_size(bool always) { m_confirm_non_default_ring_size = always; } const std::string & device_name() const { return m_device_name; } void device_name(const std::string & device_name) { m_device_name = device_name; } + const std::string & device_derivation_path() const { return m_device_derivation_path; } + void device_derivation_path(const std::string &device_derivation_path) { m_device_derivation_path = device_derivation_path; } bool get_tx_key(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector &additional_tx_keys) const; void set_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector &additional_tx_keys); @@ -1287,6 +1310,11 @@ namespace tools void setup_new_blockchain(); void create_keys_file(const std::string &wallet_, bool watch_only, const epee::wipeable_string &password, bool create_address_file); + wallet_device_callback * get_device_callback(); + void on_button_request(); + void on_pin_request(epee::wipeable_string & pin); + void on_passphrase_request(bool on_device, epee::wipeable_string & passphrase); + cryptonote::account_base m_account; boost::optional m_daemon_login; std::string m_daemon_address; @@ -1365,6 +1393,8 @@ namespace tools std::unordered_set m_scanned_pool_txs[2]; size_t m_subaddress_lookahead_major, m_subaddress_lookahead_minor; std::string m_device_name; + std::string m_device_derivation_path; + uint64_t m_device_last_key_image_sync; // Aux transaction data from device std::unordered_map m_tx_device; @@ -1398,9 +1428,10 @@ namespace tools bool m_devices_registered; std::shared_ptr m_tx_notify; + std::unique_ptr m_device_callback; }; } -BOOST_CLASS_VERSION(tools::wallet2, 26) +BOOST_CLASS_VERSION(tools::wallet2, 27) BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 10) BOOST_CLASS_VERSION(tools::wallet2::multisig_info, 1) BOOST_CLASS_VERSION(tools::wallet2::multisig_info::LR, 0) diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index b3141985d..e2caee5d2 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -219,6 +219,14 @@ namespace tools } }; //---------------------------------------------------------------------------------------------------- + struct password_entry_failed : public wallet_runtime_error + { + explicit password_entry_failed(std::string&& loc, const std::string &msg = "Password entry failed") + : wallet_runtime_error(std::move(loc), msg) + { + } + }; + //---------------------------------------------------------------------------------------------------- const char* const file_error_messages[] = { "file already exists", "file not found",