Polyseed support for 'wallet2', Wallet API, CLI wallet app, wallet RPC server
What changed, and why it matters
This commit adds support for a new type of wallet backup phrase called Polyseed to the Monero wallet software. It touches many wallet components (CLI, RPC, library API) so users can create, restore, and display Polyseed backups alongside the older 25-word legacy seeds. The change also stores a new Polyseed field in encrypted wallet files and handles passphrases/seed offsets in a specific way. There is no direct evidence in the commit that this fixes an active security bug; it reads as a large feature addition. However, because it changes how secret key material is serialized, encrypted, and derived from seed words, any mistakes in this area could affect wallet security or recoverability.
Treat this as a high-risk feature commit rather than a confirmed vulnerability. Reviewers should audit the Polyseed C++ wrapper and PBKDF2 implementation, verify that the new key-stream XOR ordering cannot corrupt existing multisig wallets, confirm that passphrase handling does not introduce confusion between encrypted Polyseeds and legacy seed offsets, and ensure the new account_keys fields are safely encrypted and wiped from memory. Fuzzing mnemonic parsing and regression-testing wallet file upgrade/downgrade paths are also recommended.
Security signals we found
Changes secret-key serialization format (m_polyseed and m_monero_c_passphrase added to account_keys KV map)
Modifies key-stream XOR encryption to include new fields, with explicit backward-compatibility ordering comment
Adds new seed-derivation path using external polyseed library and PBKDF2-HMAC-SHA256
Handles encrypted Polyseeds vs legacy seed-offset passphrases with branching logic
Adds wallet file attribute parsing for 'feather.seed' and auto-migration to Polyseed
Background sync code now restores m_polyseed from keys file after reload
Large feature patch (+1653/-280) increases attack surface in mnemonic handling
Evidence from the diff
The commit introduces Polyseed (a 16-word mnemonic scheme) integration across wallet2, the Wallet API, simplewallet, and the wallet RPC server. Key technical changes include: adding external polyseed and utf8proc submodules; a new polyseed_wrapper library with PBKDF2 and C++ bindings; extending account_keys/account_base to store and serialize m_polyseed and a compatibility passphrase_container for monero_c wallets; updating the ChaCha key-stream XOR logic to cover the new fields while preserving backward indexing for existing multisig wallets; new wallet2::generate overloads for Polyseed; updates to seed display/restore paths; and RPC command changes to expose Polyseed birthday/encryption status and language lists. The commit also refactors wallet generation helpers (prepare_generate/finish_generate) and adjusts blockchain height estimation to support Polyseed birthdays.
Changed components
src/wallet/wallet2.cpp / wallet2.hsrc/cryptonote_basic/account.cpp / account.hsrc/mnemonics/electrum-words.cpp / electrum-words.hsrc/mnemonics/polyseed/*src/simplewallet/simplewallet.cpp / simplewallet.hsrc/wallet/api/wallet.cpp / wallet.h / wallet2_api.h / wallet_manager.cpp / wallet_manager.hsrc/wallet/wallet_rpc_server.cpp / wallet_rpc_server_commands_defs.hcontrib/epee wipeable_stringexternal/polyseed and external/utf8proc submodulesInspect captured patch +1653 / −280
diff --git a/.gitmodules b/.gitmodules
index 8091f82..db787a9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -11,3 +11,9 @@
[submodule "external/gtest"]
path = external/gtest
url = https://github.com/google/googletest.git
+[submodule "external/polyseed"]
+ path = external/polyseed
+ url = https://github.com/tevador/polyseed.git
+[submodule "external/utf8proc"]
+ path = external/utf8proc
+ url = https://github.com/JuliaStrings/utf8proc.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6087605..e90308f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -400,6 +400,8 @@ if(NOT MANUAL_SUBMODULES)
check_submodule(external/rapidjson)
check_submodule(external/randomx)
check_submodule(external/supercop)
+ check_submodule(external/polyseed)
+ check_submodule(external/utf8proc)
endif()
endif()
@@ -471,7 +473,7 @@ elseif(CMAKE_SYSTEM_NAME MATCHES ".*BSDI.*")
set(BSDI TRUE)
endif()
-include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include)
+include_directories(external/rapidjson/include external/easylogging++ src contrib/epee/include external external/supercop/include external/polyseed/include external/utf8proc)
option(STATIC "Link libraries statically" OFF)
diff --git a/contrib/epee/include/wipeable_string.h b/contrib/epee/include/wipeable_string.h
index 0a324c1..69cc5c6 100644
--- a/contrib/epee/include/wipeable_string.h
+++ b/contrib/epee/include/wipeable_string.h
@@ -75,6 +75,8 @@ namespace epee
bool operator!=(const wipeable_string &other) const noexcept { return buffer != other.buffer; }
wipeable_string &operator=(wipeable_string &&other);
wipeable_string &operator=(const wipeable_string &other);
+ char& at(size_t idx);
+ const char& at(size_t idx) const;
private:
void grow(size_t sz, size_t reserved = 0);
diff --git a/contrib/epee/src/wipeable_string.cpp b/contrib/epee/src/wipeable_string.cpp
index 5d4d87d..f30a61c 100644
--- a/contrib/epee/src/wipeable_string.cpp
+++ b/contrib/epee/src/wipeable_string.cpp
@@ -261,4 +261,12 @@ wipeable_string &wipeable_string::operator=(const wipeable_string &other)
return *this;
}
+char& wipeable_string::at(size_t idx) {
+ return buffer.at(idx);
+}
+
+const char& wipeable_string::at(size_t idx) const {
+ return buffer.at(idx);
+}
+
}
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index 55339b0..58f9d21 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -32,3 +32,5 @@ add_subdirectory(db_drivers)
add_subdirectory(easylogging++)
add_subdirectory(qrcodegen)
add_subdirectory(randomx EXCLUDE_FROM_ALL)
+add_subdirectory(polyseed EXCLUDE_FROM_ALL)
+add_subdirectory(utf8proc EXCLUDE_FROM_ALL)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 05c095e..4b093e6 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -99,6 +99,7 @@ add_subdirectory(net)
add_subdirectory(hardforks)
add_subdirectory(blockchain_db)
add_subdirectory(mnemonics)
+add_subdirectory(mnemonics/polyseed)
add_subdirectory(rpc)
if(NOT IOS)
add_subdirectory(serialization)
diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp
index d0dacb0..9b19450 100644
--- a/src/cryptonote_basic/account.cpp
+++ b/src/cryptonote_basic/account.cpp
@@ -84,7 +84,7 @@ DISABLE_VS_WARNINGS(4244 4345)
void account_keys::xor_with_key_stream(const crypto::chacha_key &key)
{
// encrypt a large enough byte stream with chacha20
- epee::wipeable_string key_stream = get_key_stream(key, m_encryption_iv, sizeof(crypto::secret_key) * (2 + m_multisig_keys.size()));
+ epee::wipeable_string key_stream = get_key_stream(key, m_encryption_iv, sizeof(crypto::secret_key) * (3 + m_multisig_keys.size()) + m_monero_c_passphrase.buffer.size());
const char *ptr = key_stream.data();
for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
m_spend_secret_key.data[i] ^= *ptr++;
@@ -95,6 +95,16 @@ DISABLE_VS_WARNINGS(4244 4345)
for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
k.data[i] ^= *ptr++;
}
+
+ for (size_t i = 0; i < sizeof(crypto::secret_key); ++i)
+ m_polyseed.data[i] ^= *ptr++;
+ for (size_t i = 0; i < m_monero_c_passphrase.buffer.size(); ++i)
+ m_monero_c_passphrase.buffer.data()[i] ^= *ptr++;
+ // It's important that we xor these two new values AFTER the multisig keys. For an existing multisig wallet
+ // that was built before the introduction of Polyseed we must still xor the multisig keys with the same index
+ // values as before for things to work out correctly. For wallets written by the monero_c library we will
+ // xor these 2 NEW values still with the same indexes because those wallets are not multisig, and the
+ // empty multisig keys array does not increment the index.
}
//-----------------------------------------------------------------
void account_keys::encrypt(const crypto::chacha_key &key)
@@ -147,6 +157,7 @@ DISABLE_VS_WARNINGS(4244 4345)
{
m_keys.m_spend_secret_key = crypto::secret_key();
m_keys.m_multisig_keys.clear();
+ m_keys.m_polyseed = crypto::secret_key();
}
//-----------------------------------------------------------------
void account_base::set_spend_key(const crypto::secret_key& spend_secret_key)
@@ -160,6 +171,11 @@ DISABLE_VS_WARNINGS(4244 4345)
m_keys.m_spend_secret_key = spend_secret_key;
}
//-----------------------------------------------------------------
+ void account_base::set_polyseed(const crypto::secret_key& polyseed)
+ {
+ m_keys.m_polyseed = polyseed;
+ }
+ //-----------------------------------------------------------------
crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random)
{
crypto::secret_key first = generate_keys(m_keys.m_account_address.m_spend_public_key, m_keys.m_spend_secret_key, recovery_key, recover);
@@ -252,6 +268,18 @@ DISABLE_VS_WARNINGS(4244 4345)
create_from_keys(address, fake, viewkey);
}
//-----------------------------------------------------------------
+ void account_base::create_from_polyseed(const crypto::secret_key& spend_secret_key, const crypto::secret_key &polyseed, uint64_t polyseed_birthday)
+ {
+ generate(spend_secret_key, true, false);
+ set_createtime(polyseed_birthday);
+ // By always using the Polyseed birthday we will never get the EXACT creation time into
+ // 'm_creation_timestamp', but on the other hand we will not set it to 2014 like
+ // restore with a legacy seed does because we don't have a clue about the original
+ // creation time in that case.
+
+ m_keys.m_polyseed = polyseed;
+ }
+ //-----------------------------------------------------------------
bool account_base::make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector<crypto::secret_key> &multisig_keys)
{
m_keys.m_account_address.m_spend_public_key = spend_public_key;
diff --git a/src/cryptonote_basic/account.h b/src/cryptonote_basic/account.h
index 6242f38..c08cd62 100644
--- a/src/cryptonote_basic/account.h
+++ b/src/cryptonote_basic/account.h
@@ -46,6 +46,29 @@ namespace cryptonote
hw::device *m_device = &hw::get_device("default");
crypto::chacha_iv m_encryption_iv;
+ // Note that 'm_polyseed' is not a true 'crypto::secret_key'; it's only declared here that way for
+ // simple handling and avoiding an undesired reference of the Polyseed library, but it's a "stored"
+ // Polyseed in the sense of 'polyseed_storage'
+ crypto::secret_key m_polyseed;
+
+ // The following struct and the 'm_monero_c_passphrase' member implement a mechanism to preserve
+ // passphrases stored as a value with the key 'm_passphrase' in the JSON by wallet apps based on
+ // the 'monero_c' library, and still allow simple access for upgrading wallets. The Monero core
+ // software itself does NOT store passphrases, and completely ignores this except while reading
+ // and writing .keys files and encrypting/decrypting the key material.
+ //
+ // Note to wallet devs that used 'monero_c' and want to continue to store the passphrase: Do
+ // NOT treat this as a safe long-term storage of the passphrase. Save it early to some other
+ // place within wallet files and ignore this afterwards.
+ struct passphrase_container
+ {
+ std::vector<char> buffer;
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE_CONTAINER_POD_AS_BLOB(buffer)
+ END_KV_SERIALIZE_MAP()
+ };
+ passphrase_container m_monero_c_passphrase;
+
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(m_account_address)
KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(m_spend_secret_key)
@@ -71,6 +94,8 @@ namespace cryptonote
}
const crypto::chacha_iv default_iv{{0, 0, 0, 0, 0, 0, 0, 0}};
KV_SERIALIZE_VAL_POD_AS_BLOB_OPT(m_encryption_iv, default_iv)
+ KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(m_polyseed)
+ epee::serialization::selector<is_store>::serialize(this_ref.m_monero_c_passphrase, stg, hparent_section, "m_passphrase");
END_KV_SERIALIZE_MAP()
void encrypt(const crypto::chacha_key &key);
@@ -97,6 +122,7 @@ namespace cryptonote
void create_from_device(hw::device &hwdev);
void create_from_keys(const cryptonote::account_public_address& address, const crypto::secret_key& spendkey, const crypto::secret_key& viewkey);
void create_from_viewkey(const cryptonote::account_public_address& address, const crypto::secret_key& viewkey);
+ void create_from_polyseed(const crypto::secret_key &spend_secret_key, const crypto::secret_key &polyseed, uint64_t polyseed_birthday);
bool make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector<crypto::secret_key> &multisig_keys);
const account_keys& get_keys() const;
std::string get_public_address_str(network_type nettype) const;
@@ -111,6 +137,7 @@ namespace cryptonote
void forget_spend_key();
void set_spend_key(const crypto::secret_key& spend_secret_key);
+ void set_polyseed(const crypto::secret_key& polyseed);
const std::vector<crypto::secret_key> &get_multisig_keys() const { return m_keys.m_multisig_keys; }
void encrypt_keys(const crypto::chacha_key &key) { m_keys.encrypt(key); }
diff --git a/src/mnemonics/CMakeLists.txt b/src/mnemonics/CMakeLists.txt
index fd34bdd..ca71ff1 100644
--- a/src/mnemonics/CMakeLists.txt
+++ b/src/mnemonics/CMakeLists.txt
@@ -43,6 +43,7 @@ target_link_libraries(mnemonics
PUBLIC
epee
easylogging
+ polyseed_wrapper
${Boost_SYSTEM_LIBRARY}
PRIVATE
${EXTRA_LIBRARIES})
diff --git a/src/mnemonics/electrum-words.cpp b/src/mnemonics/electrum-words.cpp
index a3f8147..1d40531 100644
--- a/src/mnemonics/electrum-words.cpp
+++ b/src/mnemonics/electrum-words.cpp
@@ -372,6 +372,66 @@ namespace crypto
return true;
}
+ epee::wipeable_string normalize_mnemonic(const epee::wipeable_string &words)
+ {
+ epee::wipeable_string normalized;
+ bool pending_space = false;
+
+ for (size_t i = 0; i < words.size(); ++i)
+ {
+ const char c = words.data()[i];
+ // Space, CR, LF, tab, vertical tab, form feed
+ if (c == ' ' || c == '\r' || c == '\n' || c == '\t' || c == '\v' || c == '\f')
+ {
+ pending_space = !normalized.empty();
+ continue;
+ }
+
+ if (pending_space)
+ normalized.push_back(' ');
+ normalized.push_back(c);
+ pending_space = false;
+ }
+
+ return normalized;
+ }
+
+ bool words_to_bytes_ex(const epee::wipeable_string &words, crypto::secret_key& dst,
+ std::string &language_name, bool &is_polyseed, polyseed::data &polyseed)
+ {
+ is_polyseed = false;
+ polyseed::language polyseed_language;
+ const epee::wipeable_string normalized_words = normalize_mnemonic(words);
+
+ try
+ {
+ epee::wipeable_string zero_terminated_words(normalized_words);
+ zero_terminated_words.push_back('\0');
+ polyseed_language = polyseed.decode(zero_terminated_words.data());
+ is_polyseed = true;
+ }
+ catch (const polyseed::error &e)
+ {
+ if (e.status() != POLYSEED_ERR_NUM_WORDS) {
+ // Probably a Polyseed, because the number of words is ok, but with some error: Don't try as a legacy seed and stop
+ MERROR("Invalid seed: Not a valid Polyseed");
+ return false;
+ }
+ }
+ catch (const std::exception &e)
+ {
+ }
+
+ if (is_polyseed)
+ {
+ polyseed.keygen(&dst, sizeof(crypto::secret_key));
+ language_name = polyseed_language.name();
+ return true;
+ }
+
+ return words_to_bytes(normalized_words, dst, language_name);
+ }
+
/*!
* \brief Converts bytes (secret key) to seed words.
* \param src Secret key
@@ -458,13 +518,24 @@ namespace crypto
* \brief Gets a list of seed languages that are supported.
* \param languages The vector is set to the list of languages.
*/
- void get_language_list(std::vector<std::string> &languages, bool english)
+ void get_language_list(std::vector<std::string> &languages, bool english, bool polyseed)
{
- const std::vector<const Language::Base*> language_instances = get_language_list();
- for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin();
- it != language_instances.end(); it++)
+ if (polyseed)
{
- languages.push_back(english ? (*it)->get_english_language_name() : (*it)->get_language_name());
+ const std::vector<polyseed::language>& polyseed_languages = polyseed::get_langs();
+ for (auto polyseed_language: polyseed_languages)
+ {
+ languages.push_back(english ? polyseed_language.name_en() : polyseed_language.name());
+ }
+ }
+ else
+ {
+ const std::vector<const Language::Base*> language_instances = get_language_list();
+ for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin();
+ it != language_instances.end(); it++)
+ {
+ languages.push_back(english ? (*it)->get_english_language_name() : (*it)->get_language_name());
+ }
}
}
@@ -480,25 +551,52 @@ namespace crypto
return word_list.size() != (seed_length + 1);
}
- std::string get_english_name_for(const std::string &name)
+ std::string get_english_name_for(const std::string &name, bool polyseed)
{
- const std::vector<const Language::Base*> language_instances = get_language_list();
- for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin();
- it != language_instances.end(); it++)
+ if (polyseed)
{
- if ((*it)->get_language_name() == name)
- return (*it)->get_english_language_name();
+ const std::vector<polyseed::language>& polyseed_languages = polyseed::get_langs();
+ for (auto polyseed_language: polyseed_languages)
+ {
+ if (polyseed_language.name() == name)
+ {
+ return polyseed_language.name_en();
+ }
+ }
+ }
+ else
+ {
+ const std::vector<const Language::Base*> language_instances = get_language_list();
+ for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin();
+ it != language_instances.end(); it++)
+ {
+ if ((*it)->get_language_name() == name)
+ return (*it)->get_english_language_name();
+ }
}
return "<language not found>";
}
- bool is_valid_language(const std::string &language)
+ bool is_valid_language(const std::string &language, bool polyseed)
{
- const std::vector<const Language::Base*> language_instances = get_language_list();
- for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin(); it != language_instances.end(); it++)
- if ((*it)->get_english_language_name() == language || (*it)->get_language_name() == language)
- return true;
- return false;
+ if (polyseed)
+ {
+ const std::vector<polyseed::language>& polyseed_languages = polyseed::get_langs();
+ for (auto polyseed_language: polyseed_languages)
+ {
+ if (polyseed_language.name_en() == language || polyseed_language.name() == language)
+ return true;
+ }
+ return false;
+ }
+ else
+ {
+ const std::vector<const Language::Base*> language_instances = get_language_list();
+ for (std::vector<const Language::Base*>::const_iterator it = language_instances.begin(); it != language_instances.end(); it++)
+ if ((*it)->get_english_language_name() == language || (*it)->get_language_name() == language)
+ return true;
+ return false;
+ }
}
}
diff --git a/src/mnemonics/electrum-words.h b/src/mnemonics/electrum-words.h
index 374ebef..3d7c4a8 100644
--- a/src/mnemonics/electrum-words.h
+++ b/src/mnemonics/electrum-words.h
@@ -42,6 +42,7 @@
#include <string>
#include <cstdint>
#include "crypto/crypto.h" // for declaration of crypto::secret_key
+#include "polyseed/polyseed.hpp"
namespace epee { class wipeable_string; }
@@ -83,6 +84,18 @@ namespace crypto
bool words_to_bytes(const epee::wipeable_string &words, crypto::secret_key& dst,
std::string &language_name);
+ /*!
+ * \brief Converts seed words to bytes (secret key), with Polyseed support.
+ * \param words String containing the words separated by spaces.
+ * \param dst To put the secret key restored from the words.
+ * \param language_name Language of the seed as found gets written here.
+ * \param is_polyseed True if it's a Polyseed, false if it's a legacy seed
+ * \param polyseed Set to a valid Polyseed object if it's a Polyseed
+ * \return false if it is not a valid legacy seed or Polyseed
+ */
+ bool words_to_bytes_ex(const epee::wipeable_string &words, crypto::secret_key& dst,
+ std::string &language_name, bool &is_polyseed, polyseed::data &polyseed);
+
/*!
* \brief Converts bytes to seed words.
* \param src Secret data
@@ -104,12 +117,18 @@ namespace crypto
bool bytes_to_words(const crypto::secret_key& src, epee::wipeable_string& words,
const std::string &language_name);
+ /*!
+ * \brief Trim and collapse ASCII whitespace in mnemonic input, preserving UTF-8 bytes.
+ */
+ epee::wipeable_string normalize_mnemonic(const epee::wipeable_string &words);
+
/*!
* \brief Gets a list of seed languages that are supported.
* \param languages A vector is set to the list of languages.
* \param english whether to get the names in English or the language language
+ * \param polyseed whether to get the list of Polyseed languages or languages for legacy seeds
*/
- void get_language_list(std::vector<std::string> &languages, bool english = false);
+ void get_language_list(std::vector<std::string> &languages, bool english = false, bool polyseed = true);
/*!
* \brief Tells if the seed passed is an old style seed or not.
@@ -123,9 +142,9 @@ namespace crypto
* \param name the name of the language in its own language
* \return the name of the language in English
*/
- std::string get_english_name_for(const std::string &name);
+ std::string get_english_name_for(const std::string &name, bool polyseed = true);
- bool is_valid_language(const std::string &language);
+ bool is_valid_language(const std::string &language, bool polyseed = true);
}
}
diff --git a/src/mnemonics/polyseed/CMakeLists.txt b/src/mnemonics/polyseed/CMakeLists.txt
new file mode 100644
index 0000000..cf4a205
--- /dev/null
+++ b/src/mnemonics/polyseed/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(polyseed_sources
+ pbkdf2.c
+ polyseed.cpp
+)
+
+monero_find_all_headers(polyseed_private_headers "${CMAKE_CURRENT_SOURCE_DIR}")
+
+monero_private_headers(polyseed_wrapper
+ ${polyseed_private_headers}
+)
+
+monero_add_library(polyseed_wrapper
+ ${polyseed_sources}
+ ${polyseed_headers}
+ ${polyseed_private_headers}
+)
+
+target_link_libraries(polyseed_wrapper
+PUBLIC
+ polyseed_static
+ cryptonote_basic
+ utf8proc
+ sodium
+PRIVATE
+ ${EXTRA_LIBRARIES}
+)
diff --git a/src/mnemonics/polyseed/pbkdf2.c b/src/mnemonics/polyseed/pbkdf2.c
new file mode 100644
index 0000000..c6c85dd
--- /dev/null
+++ b/src/mnemonics/polyseed/pbkdf2.c
@@ -0,0 +1,85 @@
+// Copyright (c) 2026, The Monero Project
+// Copyright (c) 2021, tevador <tevador@gmail.com>
+// Copyright (c) 2005,2007,2009 Colin Percival
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+// SUCH DAMAGE.
+
+#include <string.h>
+
+#include <sodium/crypto_auth_hmacsha256.h>
+#include <sodium/utils.h>
+
+static inline void
+store32_be(uint8_t dst[4], uint32_t w)
+{
+ dst[3] = (uint8_t) w; w >>= 8;
+ dst[2] = (uint8_t) w; w >>= 8;
+ dst[1] = (uint8_t) w; w >>= 8;
+ dst[0] = (uint8_t) w;
+}
+
+void
+crypto_pbkdf2_sha256(const uint8_t* passwd, size_t passwdlen,
+ const uint8_t* salt, size_t saltlen, uint64_t c,
+ uint8_t* buf, size_t dkLen)
+{
+ crypto_auth_hmacsha256_state Phctx, PShctx, hctx;
+ size_t i;
+ uint8_t ivec[4];
+ uint8_t U[32];
+ uint8_t T[32];
+ uint64_t j;
+ int k;
+ size_t clen;
+
+ crypto_auth_hmacsha256_init(&Phctx, passwd, passwdlen);
+ PShctx = Phctx;
+ crypto_auth_hmacsha256_update(&PShctx, salt, saltlen);
+
+ for (i = 0; i * 32 < dkLen; i++) {
+ store32_be(ivec, (uint32_t)(i + 1));
+ hctx = PShctx;
+ crypto_auth_hmacsha256_update(&hctx, ivec, 4);
+ crypto_auth_hmacsha256_final(&hctx, U);
+
+ memcpy(T, U, 32);
+ for (j = 2; j <= c; j++) {
+ hctx = Phctx;
+ crypto_auth_hmacsha256_update(&hctx, U, 32);
+ crypto_auth_hmacsha256_final(&hctx, U);
+
+ for (k = 0; k < 32; k++) {
+ T[k] ^= U[k];
+ }
+ }
+
+ clen = dkLen - i * 32;
+ if (clen > 32) {
+ clen = 32;
+ }
+ memcpy(&buf[i * 32], T, clen);
+ }
+ sodium_memzero((void*)&Phctx, sizeof Phctx);
+ sodium_memzero((void*)&PShctx, sizeof PShctx);
+}
diff --git a/src/mnemonics/polyseed/pbkdf2.h b/src/mnemonics/polyseed/pbkdf2.h
new file mode 100644
index 0000000..e94c59d
--- /dev/null
+++ b/src/mnemonics/polyseed/pbkdf2.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2026, The Monero Project
+// Copyright (c) 2021, tevador <tevador@gmail.com>
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+// SUCH DAMAGE.
+
+#ifndef PBKDF2_H
+#define PBKDF2_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void
+crypto_pbkdf2_sha256(const uint8_t* passwd, size_t passwdlen,
+ const uint8_t* salt, size_t saltlen, uint64_t c,
+ uint8_t* buf, size_t dkLen);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/mnemonics/polyseed/polyseed.cpp b/src/mnemonics/polyseed/polyseed.cpp
new file mode 100644
index 0000000..11a587a
--- /dev/null
+++ b/src/mnemonics/polyseed/polyseed.cpp
@@ -0,0 +1,315 @@
+// Copyright (c) 2026, The Monero Project
+// Copyright (c) 2021, tevador <tevador@gmail.com>
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+// SUCH DAMAGE.
+
+#include "polyseed.hpp"
+#include "pbkdf2.h"
+
+#include <sodium/core.h>
+#include <sodium/utils.h>
+#include <sodium/randombytes.h>
+#include <utf8proc.h>
+
+#include <cstring>
+#include <algorithm>
+#include <array>
+
+#include "cryptonote_basic/cryptonote_format_utils.h"
+
+namespace polyseed {
+
+ inline size_t utf8_norm(const char* str, polyseed_str norm, utf8proc_option_t options) {
+ utf8proc_int32_t buffer[POLYSEED_STR_SIZE];
+ utf8proc_ssize_t result;
+
+ result = utf8proc_decompose(reinterpret_cast<const uint8_t*>(str), 0, buffer, POLYSEED_STR_SIZE, options);
+ if (result < 0 || result > (POLYSEED_STR_SIZE - 1)) {
+ throw std::runtime_error("Unicode normalization failed");
+ }
+
+ result = utf8proc_reencode(buffer, result, options);
+ if (result < 0 || result > (POLYSEED_STR_SIZE - 1)) {
+ throw std::runtime_error("Unicode normalization failed");
+ }
+
+ strcpy(norm, reinterpret_cast<const char*>(buffer));
+ sodium_memzero(buffer, sizeof(buffer));
+ return result;
+ }
+
+ static size_t utf8_nfc(const char* str, polyseed_str norm) {
+ return utf8_norm(str, norm, (utf8proc_option_t)(UTF8PROC_NULLTERM | UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_STRIPNA));
+ }
+
+ static size_t utf8_nfkd(const char* str, polyseed_str norm) {
+ return utf8_norm(str, norm, (utf8proc_option_t)(UTF8PROC_NULLTERM | UTF8PROC_STABLE | UTF8PROC_DECOMPOSE | UTF8PROC_COMPAT | UTF8PROC_STRIPNA));
+ }
+
+ struct dependency {
+ dependency();
+ std::vector<language> languages;
+ };
+
+ static dependency deps;
+
+ dependency::dependency() {
+ if (sodium_init() == -1) {
+ throw std::runtime_error("sodium_init failed");
+ }
+
+ polyseed_dependency pd;
+ pd.randbytes = &randombytes_buf;
+ pd.pbkdf2_sha256 = &crypto_pbkdf2_sha256;
+ pd.memzero = &sodium_memzero;
+ pd.u8_nfc = &utf8_nfc;
+ pd.u8_nfkd = &utf8_nfkd;
+ pd.time = nullptr;
+ pd.alloc = nullptr;
+ pd.free = nullptr;
+
+ polyseed_inject(&pd);
+
+ for (int i = 0; i < polyseed_get_num_langs(); ++i) {
+ languages.push_back(language(polyseed_get_lang(i)));
+ }
+ }
+
+ static language invalid_lang;
+
+ const std::vector<language>& get_langs() {
+ return deps.languages;
+ }
+
+ const language& get_lang_by_name(const std::string& name) {
+ for (auto& lang : deps.languages) {
+ if (name == lang.name_en()) {
+ return lang;
+ }
+ if (name == lang.name()) {
+ return lang;
+ }
+ }
+ return invalid_lang;
+ }
+
+ inline void data::check_init() const {
+ if (valid()) {
+ throw std::runtime_error("already initialized");
+ }
+ }
+
+ void data::check_valid() const {
+ if (m_data == nullptr) {
+ throw std::runtime_error("invalid object");
+ }
+ }
+
+ static std::array<const char*, 8> error_desc = {
+ "Success",
+ "Wrong number of words in the phrase",
+ "Unknown language or unsupported words",
+ "Checksum mismatch",
+ "Unsupported seed features",
+ "Invalid seed format",
+ "Memory allocation failure",
+ "Phrase matches more than one language"
+ };
+
+ static error get_error(polyseed_status status) {
+ if (status > 0 && status < sizeof(error_desc) / sizeof(const char*)) {
+ return error(error_desc[(int)status], status);
+ }
+ return error("Unknown error", status);
+ }
+
+ data::~data() {
+ if (m_data)
+ polyseed_free(m_data);
+ }
+
+ void data::create(feature_type features) {
+ check_init();
+ auto status = polyseed_create(features, &m_data);
+ if (status != POLYSEED_OK) {
+ throw get_error(status);
+ }
+ }
+
+ // Create a new random seed that is not ambiguous for sure if encoded in the given language
+ void data::create(feature_type features, const language& lang) {
+ for (; ;) {
+ create(features);
+ if (!is_ambiguous(lang)) {
+ break;
+ }
+ polyseed_free(m_data);
+ m_data = NULL;
+ }
+ }
+
+ void data::load(polyseed_storage storage) {
+ check_init();
+ auto status = polyseed_load(storage, &m_data);
+ if (status != POLYSEED_OK) {
+ throw get_error(status);
+ }
+ }
+
+ void data::load(const crypto::secret_key &key) {
+ check_init();
+ polyseed_storage d;
+ memcpy(&d, &key.data, 32);
+ auto status = polyseed_load(d, &m_data);
+ sodium_memzero(d, sizeof(d));
+ if (status != POLYSEED_OK) {
+ throw get_error(status);
+ }
+ }
+
+ language data::decode(const char* phrase) {
+ check_init();
+ const polyseed_lang* lang;
+ auto status = polyseed_decode(phrase, m_coin, &lang, &m_data);
+ if (status != POLYSEED_OK) {
+ throw get_error(status);
+ }
+ return language(lang);
+ }
+
+ // Check whether the seed, encoded in the given language, is ambiguous i.e. whether the
+ // resulting seed would be a valid seed in more than one language. Tests have shown that
+ // this is rather unlikely, but not astronomically so.
+ bool data::is_ambiguous(const language& lang) {
+ check_valid();
+ epee::wipeable_string phrase;
+ encode(lang, phrase);
+ phrase.push_back('\0');
+ polyseed::data check_polyseed(m_coin);
+ bool is_ambiguous = false;
+ try {
+ const language& check_language = check_polyseed.decode(phrase.data());
+ is_ambiguous = check_language.m_lang != lang.m_lang;
+ }
+ catch (const polyseed::error& error) {
+ if (error.status() == POLYSEED_ERR_MULT_LANG) {
+ is_ambiguous = true;
+ }
+ else {
+ throw(error);
+ }
+ }
+ return is_ambiguous;
+ }
+
+ size_t data::encode(const language& lang, char* str_out) const {
+ return polyseed_encode(m_data, lang.m_lang, m_coin, str_out);
+ }
+
+ void data::save(polyseed_storage storage) const {
+ check_valid();
+ polyseed_store(m_data, storage);
+ }
+
+ void data::save(void *storage) const {
+ check_valid();
+ polyseed_store(m_data, (uint8_t*)storage);
+ }
+
+ void data::crypt(const char* password) {
+ check_valid();
+ polyseed_crypt(m_data, password);
+ }
+
+ void data::keygen(void* ptr, size_t key_size) const {
+ check_valid();
+ polyseed_keygen(m_data, m_coin, key_size, (uint8_t*)ptr);
+ }
+
+ // Generate a key to serve as the spend secret key, given Polyseed data and an optional passphrase / seed offset;
+ //
+ // We use the following restore strategy regarding passphrase: If the seed is encrypted in the sense of the
+ // Polyseed 'Encrypted' feature bit, we take the passphrase to be the password / decryption key and decrypt.
+ // If the Polyseed is not encrypted, we use the passphrase to do the usual "seed offsetting" as we do it
+ // already for a long time in the core software with secret keys defined through 25 word legacy seeds.
+ //
+ // Although strictly speaking seed offsetting is not part of the Polyseed spec, various third-party wallet
+ // apps do it already in this way for quite some time, and there are zero technical problems with it,
+ // thus we follow this as some sort of "de facto standard".
+ //
+ // We support encrypted Polyseeds here because an important third-party wallet app does NOT do seed
+ // offsetting if the user specifies a passphrase at new wallet creation, but encrypts the seed with it,
+ // and we want to be able to restore wallets from those seeds. However, we don't ever produce encrypted
+ // Polyseeds ourselves in the Monero core code and always offset the seed with any giving passphrase.
+ //
+ // Said third-party wallet app uses, as far as RESTORING from Polyseed with optional passphrase is
+ // concerned, exactly the same strategy as implemented here.
+ crypto::secret_key data::generate_secret_key(const epee::wipeable_string &passphrase) const
+ {
+ bool is_encrypted = encrypted();
+ bool has_passphrase = !passphrase.empty();
+
+ polyseed_storage storage;
+ save(storage);
+ polyseed::data seed_copy(m_coin);
+ seed_copy.load(storage);
+ sodium_memzero(storage, sizeof(storage));
+
+ if (is_encrypted) {
+ if (!has_passphrase) {
+ throw std::runtime_error("encrypted Polyseed needs a passphrase");
+ }
+ epee::wipeable_string zero_terminated_passphrase(passphrase);
+ zero_terminated_passphrase.push_back('\0');
+ seed_copy.crypt(zero_terminated_passphrase.data());
+ }
+ crypto::secret_key secret_key;
+ seed_copy.keygen(&secret_key, sizeof(secret_key));
+
+ if (!is_encrypted && has_passphrase) {
+ secret_key = cryptonote::decrypt_key(secret_key, passphrase);
+ }
+ return secret_key;
+ }
+
+ bool data::valid() const {
+ return m_data != nullptr;
+ }
+
+ bool data::encrypted() const {
+ check_valid();
+ return polyseed_is_encrypted(m_data);
+ }
+
+ uint64_t data::birthday() const {
+ check_valid();
+ return polyseed_get_birthday(m_data);
+ }
+
+ bool data::has_feature(feature_type feature) const {
+ check_valid();
+ return polyseed_get_feature(m_data, feature) != 0;
+ }
+
+}
diff --git a/src/mnemonics/polyseed/polyseed.hpp b/src/mnemonics/polyseed/polyseed.hpp
new file mode 100644
index 0000000..44751cb
--- /dev/null
+++ b/src/mnemonics/polyseed/polyseed.hpp
@@ -0,0 +1,144 @@
+// Copyright (c) 2026, The Monero Project
+// Copyright (c) 2021, tevador <tevador@gmail.com>
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+// SUCH DAMAGE.
+
+#pragma once
+
+#include <polyseed.h>
+#include <polyseed/src/lang.h>
+#include <vector>
+#include <stdexcept>
+#include <string>
+#include "crypto/crypto.h"
+#include "wipeable_string.h"
+
+namespace polyseed {
+
+ class data;
+
+ class language {
+ public:
+ language() : m_lang(nullptr) {}
+ language(const language&) = default;
+ language(const polyseed_lang* lang) : m_lang(lang) {}
+ const char* name() const {
+ return polyseed_get_lang_name(m_lang);
+ }
+ const char* name_en() const {
+ return polyseed_get_lang_name_en(m_lang);
+ }
+ const char* separator() const {
+ return m_lang->separator;
+ }
+ bool valid() const {
+ return m_lang != nullptr;
+ }
+
+ const polyseed_lang* m_lang;
+ private:
+
+ friend class data;
+ };
+
+ const std::vector<language>& get_langs();
+ const language& get_lang_by_name(const std::string& name);
+
+ class error : public std::runtime_error {
+ public:
+ error(const char* msg, polyseed_status status)
+ : std::runtime_error(msg), m_status(status)
+ {
+ }
+ polyseed_status status() const {
+ return m_status;
+ }
+ private:
+ polyseed_status m_status;
+ };
+
+ using feature_type = unsigned int;
+
+ inline int enable_features(feature_type features) {
+ return polyseed_enable_features(features);
+ }
+
+ class data {
+ public:
+ data(const data&) = delete;
+ data(polyseed_coin coin) : m_data(nullptr), m_coin(coin) {}
+ ~data();
+
+ void create(feature_type features);
+
+ void create(feature_type features, const language& lang);
+
+ void load(polyseed_storage storage);
+
+ void load(const crypto::secret_key &key);
+
+ language decode(const char* phrase);
+
+ bool is_ambiguous(const language& lang);
+
+ size_t encode(const language& lang, char* str_out) const;
+
+ template<class str_type>
+ void encode(const language& lang, str_type& str) const {
+ check_valid();
+ if (!lang.valid()) {
+ throw std::runtime_error("invalid language");
+ }
+ str.resize(POLYSEED_STR_SIZE);
+ auto size = encode(lang.m_lang, &str.at(0));
+ str.resize(size);
+ }
+
+ void save(polyseed_storage storage) const;
+
+ void save(void *storage) const;
+
+ void crypt(const char* password);
+
+ void keygen(void* ptr, size_t key_size) const;
+
+ crypto::secret_key generate_secret_key(const epee::wipeable_string &passphrase) const;
+
+ bool valid() const;
+
+ bool encrypted() const;
+
+ uint64_t birthday() const;
+
+ bool has_feature(feature_type feature) const;
+
+ private:
+ void check_valid() const;
+
+ void check_init() const;
+
+ polyseed_data* m_data;
+ polyseed_coin m_coin;
+ };
+}
diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp
index bca7db4..73de3fe 100644
--- a/src/simplewallet/simplewallet.cpp
+++ b/src/simplewallet/simplewallet.cpp
@@ -175,6 +175,7 @@ namespace
const command_line::arg_descriptor<bool> arg_create_address_file = {"create-address-file", sw::tr("Create an address file for new wallets"), false};
const command_line::arg_descriptor<std::string> arg_subaddress_lookahead = {"subaddress-lookahead", tools::wallet2::tr("Set subaddress lookahead sizes to <major>:<minor>"), ""};
const command_line::arg_descriptor<bool> arg_use_english_language_names = {"use-english-language-names", sw::tr("Display English language names"), false};
+ const command_line::arg_descriptor<bool> arg_use_legacy_seed = {"use-legacy-seed", sw::tr("Use 25 word legacy seed instead of Polyseed"), false};
const command_line::arg_descriptor< std::vector<std::string> > arg_command = {"command", ""};
@@ -858,7 +859,7 @@ bool simple_wallet::spendkey(const std::vector<std::string> &args/* = std::vecto
return true;
}
-bool simple_wallet::print_seed(bool encrypted)
+bool simple_wallet::print_seed(bool encrypted, bool as_legacy_seed)
{
bool success = false;
epee::wipeable_string seed;
@@ -883,6 +884,21 @@ bool simple_wallet::print_seed(bool encrypted)
fail_msg_writer() << tr("wallet is multisig but not yet finalized");
return true;
}
+ if (as_legacy_seed)
+ {
+ fail_msg_writer() << tr("wallet is multisig and thus has no legacy seed");
+ return true;
+ }
+ }
+ if (as_legacy_seed && !m_wallet->is_polyseed())
+ {
+ fail_msg_writer() << tr("legacy seed display only possible for wallet with Polyseed");
+ return true;
+ }
+ if (encrypted && m_wallet->is_polyseed())
+ {
+ fail_msg_writer() << tr("wallet has a Polyseed which can't get encrypted using this command");
+ return true;
}
SCOPED_WALLET_UNLOCK();
@@ -894,6 +910,8 @@ bool simple_wallet::print_seed(bool encrypted)
}
epee::wipeable_string seed_pass;
+ uint64_t birthday = 0;
+ bool is_encrypted = false;
if (encrypted)
{
auto pwd_container = password_prompter(tr("Enter optional seed offset passphrase, empty to see raw seed"), true);
@@ -905,11 +923,29 @@ bool simple_wallet::print_seed(bool encrypted)
if (ms_status.multisig_is_active)
success = m_wallet->get_multisig_seed(seed, seed_pass);
else if (m_wallet->is_deterministic())
- success = m_wallet->get_seed(seed, seed_pass);
+ {
+ if (m_wallet->is_polyseed())
+ {
+ if (as_legacy_seed)
+ {
+ // We may have a Polyseed in a language that legacy seeds don't support, or have a different name
+ // for, thus always give out a "legacy seed" in English to avoid any problems
+ success = m_wallet->get_seed(seed, "", true);
+ }
+ else
+ {
+ success = m_wallet->get_polyseed(seed, birthday, is_encrypted);
+ }
+ }
+ else
+ {
+ success = m_wallet->get_seed(seed, seed_pass);
+ }
+ }
if (success)
{
- print_seed(seed);
+ print_seed(seed, as_legacy_seed, birthday, is_encrypted);
}
else
{
@@ -920,12 +956,17 @@ bool simple_wallet::print_seed(bool encrypted)
bool simple_wallet::seed(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
- return print_seed(false);
+ return print_seed(false, false);
}
bool simple_wallet::encrypted_seed(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
- return print_seed(true);
+ return print_seed(true, false);
+}
+
+bool simple_wallet::legacy_seed(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
+{
+ return print_seed(false, true);
}
bool simple_wallet::restore_height(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
@@ -976,7 +1017,7 @@ bool simple_wallet::seed_set_language(const std::vector<std::string> &args/* = s
password = pwd_container->password();
}
- std::string mnemonic_language = get_mnemonic_language();
+ std::string mnemonic_language = get_mnemonic_language(m_wallet->is_polyseed());
if (mnemonic_language.empty())
return true;
@@ -3140,6 +3181,9 @@ simple_wallet::simple_wallet()
m_cmd_binder.set_handler("seed",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::seed, _1),
tr("Display the Electrum-style mnemonic seed"));
+ m_cmd_binder.set_handler("legacy_seed",
+ boost::bind(&simple_wallet::on_command, this, &simple_wallet::legacy_seed, _1),
+ tr("Display a Polyseed as a legacy seed"));
m_cmd_binder.set_handler("restore_height",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::restore_height, _1),
tr("Display the restore height"));
@@ -3550,7 +3594,7 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
{
std::string seed_language = m_wallet->get_seed_language();
if (m_use_english_language_names)
- seed_language = crypto::ElectrumWords::get_english_name_for(seed_language);
+ seed_language = crypto::ElectrumWords::get_english_name_for(seed_language, m_wallet->is_polyseed());
std::string priority_string = "invalid";
const fee_priority priority = m_wallet->get_default_priority();
priority_string = tools::fee_priority_utilities::to_string(priority);
@@ -3801,37 +3845,53 @@ bool simple_wallet::ask_wallet_create_if_needed(const std::string &wallet_dir)
* \brief Prints the seed with a nice message
* \param seed seed to print
*/
-void simple_wallet::print_seed(const epee::wipeable_string &seed)
+void simple_wallet::print_seed(const epee::wipeable_string &seed, bool as_legacy_seed, uint64_t birthday, bool is_encrypted)
{
- success_msg_writer(true) << "\n" << boost::format(tr("NOTE: the following %s can be used to recover access to your wallet. "
- "Write them down and store them somewhere safe and secure. Please do not store them in "
- "your email or on file storage services outside of your immediate control.\n")) % (m_wallet->get_multisig_status().multisig_is_active ? tr("string") : tr("25 words"));
+ std::string seed_type;
+ if (m_wallet->get_multisig_status().multisig_is_active)
+ {
+ seed_type = tr("string");
+ }
+ else if (m_wallet->is_polyseed() && !as_legacy_seed)
+ {
+ seed_type = tr("16 word Polyseed");
+ }
+ else
+ {
+ seed_type = tr("25 word legacy seed");
+ }
+ success_msg_writer(true) << "\n" << boost::format(tr("NOTE: The following %s can be used to recover access to your wallet. "
+ "Write this info down and store it somewhere safe and secure. Please do not store it in "
+ "your email or on file storage services outside of your immediate control.\n")) % seed_type;
+ if (as_legacy_seed)
+ {
+ success_msg_writer(true) << tr("Use the following English legacy seed, without any seed offset, to restore if you can't use the wallet's original Polyseed:\n");
+ }
// don't log
- int space_index = 0;
- size_t len = seed.size();
- for (const char *ptr = seed.data(); len--; ++ptr)
+ if (m_wallet->is_polyseed())
{
- if (*ptr == ' ')
+ epee::wipeable_string zero_terminated_seed(seed);
+ zero_terminated_seed.push_back('\0');
+ std::cout << zero_terminated_seed.data() << std::endl << std::endl;
+ if (is_encrypted)
{
- if (space_index == 15 || space_index == 7)
- putchar('\n');
- else
- putchar(*ptr);
- ++space_index;
+ std::cout << tr("Polyseed is ENCRYPTED using a passphrase that is mandatory for restoring") << std::endl;
}
- else
+ if (birthday != 0)
+ {
+ std::cout << tr("Polyseed birthday: ") << tools::get_human_readable_timestamp(birthday) << std::endl;
+ }
+ }
+ else
+ {
+ size_t len = seed.size();
+ for (const char *ptr = seed.data(); len--; ++ptr)
+ {
putchar(*ptr);
+ }
+ putchar('\n');
+ fflush(stdout);
}
- putchar('\n');
- fflush(stdout);
-}
-//----------------------------------------------------------------------------------------------------
-static bool might_be_partial_seed(const epee::wipeable_string &words)
-{
- std::vector<epee::wipeable_string> seed;
-
- words.split(seed);
- return seed.size() < 24;
}
//----------------------------------------------------------------------------------------------------
static bool datestr_to_int(const std::string &heightstr, uint16_t &year, uint8_t &month, uint8_t &day)
@@ -3908,6 +3968,9 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
return false;
std::string old_language;
+ bool is_polyseed = !m_restoring && !m_non_deterministic && !m_use_legacy_seed;
+ polyseed::data polyseed(POLYSEED_MONERO);
+
// check for recover flag. if present, require electrum word list (only recovery option for now).
if (m_restore_deterministic_wallet || m_restore_multisig_wallet)
{
@@ -3942,20 +4005,16 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
else
{
m_electrum_seed = "";
- do
+ const char *prompt = "Specify Electrum seed";
+ epee::wipeable_string electrum_seed = input_secure_line(prompt);
+ if (std::cin.eof())
+ return false;
+ if (electrum_seed.empty())
{
- const char *prompt = m_electrum_seed.empty() ? "Specify Electrum seed" : "Electrum seed continued";
- epee::wipeable_string electrum_seed = input_secure_line(prompt);
- if (std::cin.eof())
- return false;
- if (electrum_seed.empty())
- {
- fail_msg_writer() << tr("specify a recovery parameter with the --electrum-seed=\"words list here\"");
- return false;
- }
- m_electrum_seed += electrum_seed;
- m_electrum_seed += ' ';
- } while (might_be_partial_seed(m_electrum_seed));
+ fail_msg_writer() << tr("specify a recovery parameter with the --electrum-seed=\"words list here\"");
+ return false;
+ }
+ m_electrum_seed = electrum_seed;
}
}
@@ -3971,7 +4030,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
}
else
{
- if (!crypto::ElectrumWords::words_to_bytes(m_electrum_seed, m_recovery_key, old_language))
+ if (!crypto::ElectrumWords::words_to_bytes_ex(m_electrum_seed, m_recovery_key, old_language, is_polyseed, polyseed))
{
fail_msg_writer() << tr("Electrum-style word list failed verification");
return false;
@@ -3982,6 +4041,12 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
if (std::cin.eof() || !pwd_container)
return false;
seed_pass = pwd_container->password();
+ if (is_polyseed && !seed_pass.empty())
+ {
+ message_writer(console_color_red, false) << tr("You will have to provide this passphrase again if you restore again.");
+ message_writer(console_color_red, false) << tr("It does not get stored, and you can't re-display it in the app.");
+ message_writer(console_color_red, false) << tr("A wrong passphrase won't get detected, it will just result in a different wallet.");
+ }
if (!seed_pass.empty() && !m_restore_multisig_wallet)
m_recovery_key = cryptonote::decrypt_key(m_recovery_key, seed_pass);
}
@@ -4055,7 +4120,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
fail_msg_writer() << tr("failed to parse spend key secret key");
return false;
}
- auto r = new_wallet(vm, m_recovery_key, true, false, "");
+ auto r = new_wallet(vm, m_recovery_key, true, false, "", false, polyseed, seed_pass);
CHECK_AND_ASSERT_MES(r, false, tr("account creation failed"));
password = *r;
welcome = true;
@@ -4346,7 +4411,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
if (m_restore_multisig_wallet)
r = new_wallet(vm, multisig_keys, seed_pass, old_language);
else
- r = new_wallet(vm, m_recovery_key, m_restore_deterministic_wallet, m_non_deterministic, old_language);
+ r = new_wallet(vm, m_recovery_key, m_restore_deterministic_wallet, m_non_deterministic, old_language, is_polyseed, polyseed, seed_pass);
CHECK_AND_ASSERT_MES(r, false, tr("account creation failed"));
password = *r;
welcome = true;
@@ -4354,9 +4419,31 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
if (m_restoring && m_generate_from_json.empty() && m_generate_from_device.empty())
{
- m_wallet->explicit_refresh_from_block_height(!(command_line::is_arg_defaulted(vm, arg_restore_height) &&
- command_line::is_arg_defaulted(vm, arg_restore_date)));
- if (command_line::is_arg_defaulted(vm, arg_restore_height) && !command_line::is_arg_defaulted(vm, arg_restore_date))
+ bool restore_height_defaulted = command_line::is_arg_defaulted(vm, arg_restore_height);
+ bool restore_date_defaulted = command_line::is_arg_defaulted(vm, arg_restore_date);
+ m_wallet->explicit_refresh_from_block_height(!restore_height_defaulted || !restore_date_defaulted || is_polyseed);
+
+ uint64_t polyseed_restore_height = 0;
+ bool is_override = false;
+
+ if (is_polyseed)
+ {
+ polyseed_restore_height = m_wallet->estimate_blockchain_height(polyseed.birthday());
+ if (!restore_height_defaulted || !restore_date_defaulted)
+ {
+ is_override = true;
+ message_writer(console_color_red, true) <<
+ boost::format(tr("--restore-height or --restore-date parameter value overrides restore height %u from Polyseed birthday")) % polyseed_restore_height;
+ message_writer(console_color_red, true) <<
+ tr("With a Polyseed you don't have to specify your own restore height, and usually you don't specify any");
+ }
+ else
+ {
+ m_restore_height = polyseed_restore_height;
+ }
+ }
+
+ if (restore_height_defaulted && !restore_date_defaulted)
{
uint16_t year;
uint8_t month;
@@ -4374,6 +4461,12 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
return false;
}
}
+
+ if (is_override && m_restore_height > polyseed_restore_height)
+ {
+ message_writer(console_color_red, true) <<
+ boost::format(tr("Restore height %u is higher than the restore height from Polyseed birthday, make sure to not miss any transfers")) % m_restore_height;
+ }
}
if (!m_wallet->explicit_refresh_from_block_height() && m_restoring)
{
@@ -4518,6 +4611,7 @@ bool simple_wallet::handle_command_line(const boost::program_options::variables_
m_non_deterministic = command_line::get_arg(vm, arg_non_deterministic);
m_restore_height = command_line::get_arg(vm, arg_restore_height);
m_restore_date = command_line::get_arg(vm, arg_restore_date);
+ m_use_legacy_seed = command_line::get_arg(vm, arg_use_legacy_seed);
m_do_not_relay = command_line::get_arg(vm, arg_do_not_relay);
m_subaddress_lookahead = command_line::get_arg(vm, arg_subaddress_lookahead);
m_use_english_language_names = command_line::get_arg(vm, arg_use_english_language_names);
@@ -4584,14 +4678,14 @@ bool simple_wallet::try_connect_to_daemon(bool silent, uint32_t* version)
*
* \return The chosen language.
*/
-std::string simple_wallet::get_mnemonic_language()
+std::string simple_wallet::get_mnemonic_language(bool polyseed)
{
std::vector<std::string> language_list_self, language_list_english;
const std::vector<std::string> &language_list = m_use_english_language_names ? language_list_english : language_list_self;
std::string language_choice;
int language_number = -1;
- crypto::ElectrumWords::get_language_list(language_list_self, false);
- crypto::ElectrumWords::get_language_list(language_list_english, true);
+ crypto::ElectrumWords::get_language_list(language_list_self, false, polyseed);
+ crypto::ElectrumWords::get_language_list(language_list_english, true, polyseed);
std::cout << tr("List of available languages for your wallet's seed:") << std::endl;
std::cout << tr("If your display freezes, exit blind with ^C, then run again with --use-english-language-names") << std::endl;
int ii;
@@ -4644,7 +4738,8 @@ boost::optional<tools::password_container> simple_wallet::get_and_verify_passwor
}
//----------------------------------------------------------------------------------------------------
boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::program_options::variables_map& vm,
- const crypto::secret_key& recovery_key, bool recover, bool two_random, const std::string &old_language)
+ const crypto::secret_key& recovery_key, bool recover, bool two_random, const std::string &old_language,
+ bool is_polyseed, polyseed::data &polyseed, const epee::wipeable_string &seed_pass)
{
std::pair<std::unique_ptr<tools::wallet2>, tools::password_container> rc;
try { rc = tools::wallet2::make_new(vm, false, password_prompter); }
@@ -4668,8 +4763,9 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr
std::string mnemonic_language = old_language;
+ // Check for mnemonic language from command line argument
std::vector<std::string> language_list;
- crypto::ElectrumWords::get_language_list(language_list);
+ crypto::ElectrumWords::get_language_list(language_list, false, is_polyseed);
if (mnemonic_language.empty() && std::find(language_list.begin(), language_list.end(), m_mnemonic_language) != language_list.end())
{
mnemonic_language = m_mnemonic_language;
@@ -4688,7 +4784,7 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr
message_writer(console_color_green, false) << "\n" << tr("You had been using "
"a deprecated version of the wallet. Please use the new seed that we provide.\n");
}
- mnemonic_language = get_mnemonic_language();
+ mnemonic_language = get_mnemonic_language(is_polyseed);
if (mnemonic_language.empty())
return {};
}
@@ -4700,7 +4796,17 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr
crypto::secret_key recovery_val;
try
{
- recovery_val = m_wallet->generate(m_wallet_file, std::move(rc.second).password(), recovery_key, recover, two_random, create_address_file);
+ if (is_polyseed)
+ {
+ if (!recover)
+ {
+ polyseed.create(0, polyseed::get_lang_by_name(mnemonic_language));
+ }
+ m_wallet->generate(m_wallet_file, std::move(rc.second).password(), polyseed, seed_pass, recover, m_restore_height, create_address_file);
+ }
+ else {
+ recovery_val = m_wallet->generate(m_wallet_file, std::move(rc.second).password(), recovery_key, recover, two_random, create_address_file);
+ }
message_writer(console_color_white, true) << tr("Generated new wallet: ")
<< m_wallet->get_account().get_public_address_str(m_wallet->nettype());
PAUSE_READLINE();
@@ -4717,7 +4823,15 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr
// convert rng value to electrum-style word list
epee::wipeable_string electrum_words;
- crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language);
+ if (is_polyseed)
+ {
+ polyseed::language polyseed_language = polyseed::get_lang_by_name(mnemonic_language);
+ polyseed.encode(polyseed_language, electrum_words);
+ }
+ else
+ {
+ crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language);
+ }
success_msg_writer() <<
"**********************************************************************\n" <<
@@ -4734,7 +4848,7 @@ boost::optional<epee::wipeable_string> simple_wallet::new_wallet(const boost::pr
if (!two_random)
{
- print_seed(electrum_words);
+ print_seed(electrum_words, false, 0, false);
}
success_msg_writer() << "**********************************************************************";
@@ -4957,7 +5071,7 @@ boost::optional<epee::wipeable_string> simple_wallet::open_wallet(const boost::p
{
message_writer(console_color_green, false) << "\n" << tr("You had been using "
"a deprecated version of the wallet. Please proceed to upgrade your wallet.\n");
- std::string mnemonic_language = get_mnemonic_language();
+ std::string mnemonic_language = get_mnemonic_language(false);
if (mnemonic_language.empty())
return {};
m_wallet->set_seed_language(mnemonic_language);
@@ -4966,7 +5080,7 @@ boost::optional<epee::wipeable_string> simple_wallet::open_wallet(const boost::p
// Display the seed
epee::wipeable_string seed;
m_wallet->get_seed(seed);
- print_seed(seed);
+ print_seed(seed, false, 0, false);
}
else
{
@@ -4975,7 +5089,45 @@ boost::optional<epee::wipeable_string> simple_wallet::open_wallet(const boost::p
m_wallet->rewrite(m_wallet_file, password);
}
}
+
+ if (!m_wallet->is_polyseed() && !ms_status.multisig_is_active)
+ {
+ std::string seed;
+ bool has_feather_seed = m_wallet->get_attribute("feather.seed", seed);
+ // We ignore the "feather.seedoffset" attribute as we, unlike Feather Wallet, don't store passphrases
+ if (has_feather_seed)
+ {
+ polyseed::data polyseed(POLYSEED_MONERO);
+ polyseed::language lang;
+ bool is_valid_polyseed = false;
+ try
+ {
+ lang = polyseed.decode(seed.c_str());
+ is_valid_polyseed = true;
+ }
+ catch (const std::exception& e)
+ {
+ }
+ if (is_valid_polyseed)
+ {
+ // As yet unmodified wallet file written by Feather Wallet app: Switch to Polyseed "in our way";
+ // file stays compatible with Feather
+ m_wallet->set_seed_language(lang.name());
+ crypto::secret_key polyseed_storage;
+ polyseed.save(&polyseed_storage);
+ {
+ tools::wallet_keys_unlocker unlocker(*m_wallet, &password);
+ m_wallet->get_account().set_polyseed(polyseed_storage);
+ }
+ m_wallet->set_is_polyseed(true);
+ m_wallet->rewrite(m_wallet_file, password);
+
+ }
+ memwipe(seed.data(), seed.size());
+ }
+ }
}
+
catch (const std::exception& e)
{
fail_msg_writer() << tr("failed to load wallet: ") << e.what();
@@ -9743,6 +9895,19 @@ bool simple_wallet::wallet_info(const std::vector<std::string> &args)
message_writer() << tr("Network type: ") << (
m_wallet->nettype() == cryptonote::TESTNET ? tr("Testnet") :
m_wallet->nettype() == cryptonote::STAGENET ? tr("Stagenet") : tr("Mainnet"));
+ if (ms_status.multisig_is_active)
+ {
+ type = tr("Multisig");
+ }
+ else if (m_wallet->is_polyseed())
+ {
+ type = tr("Polyseed");
+ }
+ else
+ {
+ type = tr("Legacy");
+ }
+ message_writer() << tr("Seed type: ") << type;
return true;
}
//----------------------------------------------------------------------------------------------------
@@ -10332,6 +10497,7 @@ int main(int argc, char* argv[])
command_line::add_arg(desc_params, arg_electrum_seed );
command_line::add_arg(desc_params, arg_restore_height);
command_line::add_arg(desc_params, arg_restore_date);
+ command_line::add_arg(desc_params, arg_use_legacy_seed);
command_line::add_arg(desc_params, arg_do_not_relay);
command_line::add_arg(desc_params, arg_create_address_file);
command_line::add_arg(desc_params, arg_subaddress_lookahead);
diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h
index 1d1ac8a..c2bad89 100644
--- a/src/simplewallet/simplewallet.h
+++ b/src/simplewallet/simplewallet.h
@@ -97,7 +97,7 @@ namespace cryptonote
boost::optional<tools::password_container> get_and_verify_password(bool *read_failed = nullptr) const;
boost::optional<epee::wipeable_string> new_wallet(const boost::program_options::variables_map& vm, const crypto::secret_key& recovery_key,
- bool recover, bool two_random, const std::string &old_language);
+ bool recover, bool two_random, const std::string &old_language, bool is_polyseed, polyseed::data &polyseed, const epee::wipeable_string &seed_pass);
boost::optional<epee::wipeable_string> new_wallet(const boost::program_options::variables_map& vm, const cryptonote::account_public_address& address,
const boost::optional<crypto::secret_key>& spendkey, const crypto::secret_key& viewkey);
boost::optional<epee::wipeable_string> new_wallet(const boost::program_options::variables_map& vm,
@@ -110,6 +110,7 @@ namespace cryptonote
bool spendkey(const std::vector<std::string> &args = std::vector<std::string>());
bool seed(const std::vector<std::string> &args = std::vector<std::string>());
bool encrypted_seed(const std::vector<std::string> &args = std::vector<std::string>());
+ bool legacy_seed(const std::vector<std::string> &args = std::vector<std::string>());
bool restore_height(const std::vector<std::string> &args = std::vector<std::string>());
/*!
@@ -268,7 +269,7 @@ namespace cryptonote
bool accept_loaded_tx(const tools::wallet2::signed_tx_set &txs);
bool process_ring_members(const std::vector<tools::wallet2::pending_tx>& ptx_vector, std::ostream& ostr, bool verbose);
std::string get_prompt() const;
- bool print_seed(bool encrypted);
+ bool print_seed(bool encrypted, bool as_legacy_seed);
void key_images_sync_intern();
void on_refresh_finished(uint64_t start_height, uint64_t fetched_blocks, bool is_init, bool received_money);
std::pair<std::string, std::string> show_outputs_line(const std::vector<uint64_t> &heights, uint64_t blockchain_height, uint64_t highlight_idx = std::numeric_limits<uint64_t>::max()) const;
@@ -301,7 +302,7 @@ namespace cryptonote
* \brief Prints the seed with a nice message
* \param seed seed to print
*/
- void print_seed(const epee::wipeable_string &seed);
+ void print_seed(const epee::wipeable_string &seed, bool as_legacy_seed, uint64_t birthday, bool is_encrypted);
/*!
* \brief Gets the word seed language from the user.
@@ -310,7 +311,7 @@ namespace cryptonote
*
* \return The chosen language.
*/
- std::string get_mnemonic_language();
+ std::string get_mnemonic_language(bool polyseed);
/*!
* \brief When --do-not-relay option is specified, save the raw tx hex blob to a file instead of calling m_wallet->commit_tx(ptx).
@@ -421,6 +422,7 @@ namespace cryptonote
bool m_non_deterministic; // old 2-random generation
bool m_restoring; // are we restoring, by whatever method?
uint64_t m_restore_height; // optional
+ bool m_use_legacy_seed;
bool m_do_not_relay;
bool m_use_english_language_names;
diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
index 841d1d1..1ec1420 100644
--- a/src/wallet/api/wallet.cpp
+++ b/src/wallet/api/wallet.cpp
@@ -705,6 +705,30 @@ bool WalletImpl::recoverFromDevice(const std::string &path, const std::string &p
return true;
}
+bool WalletImpl::createFromPolyseed(const std::string &path, const std::string &password, const std::string &seed,
+ const std::string &passphrase, bool newWallet, uint64_t restoreHeight)
+{
+ clearStatus();
+ m_recoveringFromSeed = !newWallet;
+ m_recoveringFromDevice = false;
+
+ polyseed::data polyseed(POLYSEED_MONERO);
+
+ try {
+ auto normalized_seed = crypto::ElectrumWords::normalize_mnemonic(seed);
+ normalized_seed.push_back('\0');
+ auto lang = polyseed.decode(normalized_seed.data());
+ m_wallet->set_seed_language(lang.name());
+ m_wallet->generate(path, password, polyseed, passphrase, !newWallet, restoreHeight);
+ }
+ catch (const std::exception &e) {
+ setStatusCritical(e.what());
+ return false;
+ }
+ m_password = password;
+ return true;
+}
+
Wallet::Device WalletImpl::getDeviceType() const
{
return static_cast<Wallet::Device>(m_wallet->get_device_type());
@@ -755,25 +779,42 @@ bool WalletImpl::recover(const std::string &path, const std::string &password, c
m_recoveringFromDevice = false;
crypto::secret_key recovery_key;
std::string old_language;
- if (!crypto::ElectrumWords::words_to_bytes(seed, recovery_key, old_language)) {
+ bool is_polyseed;
+ polyseed::data polyseed(POLYSEED_MONERO);
+ if (!crypto::ElectrumWords::words_to_bytes_ex(seed, recovery_key, old_language, is_polyseed, polyseed)) {
setStatusError(tr("Electrum-style word list failed verification"));
return false;
}
- if (!seed_offset.empty())
- {
- recovery_key = cryptonote::decrypt_key(recovery_key, seed_offset);
+ if (is_polyseed) {
+ try {
+ const polyseed::language &lang = polyseed::get_lang_by_name(old_language);
+ m_wallet->set_seed_language(lang.name());
+ m_wallet->generate(path, password, polyseed, seed_offset, true, m_wallet->get_refresh_from_block_height());
+ // Note to callers of this 'recover' method: Having a need to OVERRIDE the refresh height derived
+ // from the Polyseed birthday should be an exception, usually you do NOT set the restore height
+ // yourself beforehand if a Polyseed gets used.
+ m_password = password;
+ }
+ catch (const std::exception &e) {
+ setStatusCritical(e.what());
+ return false;
+ }
}
+ else {
+ if (!seed_offset.empty())
+ recovery_key = cryptonote::decrypt_key(recovery_key, seed_offset);
- if (old_language == crypto::ElectrumWords::old_language_name)
- old_language = Language::English().get_language_name();
+ if (old_language == crypto::ElectrumWords::old_language_name)
+ old_language = Language::English().get_language_name();
- try {
- m_wallet->set_seed_language(old_language);
- m_wallet->generate(path, password, recovery_key, true, false);
- m_password = password;
+ try {
+ m_wallet->set_seed_language(old_language);
+ m_wallet->generate(path, password, recovery_key, true, false);
+ m_password = password;
- } catch (const std::exception &e) {
- setStatusCritical(e.what());
+ } catch (const std::exception &e) {
+ setStatusCritical(e.what());
+ }
}
return status() == Status_Ok;
}
@@ -812,10 +853,60 @@ std::string WalletImpl::seed(const std::string& seed_offset) const
return std::string();
epee::wipeable_string seed;
if (m_wallet)
- m_wallet->get_seed(seed, seed_offset);
+ m_wallet->get_seed(seed, seed_offset, m_wallet->is_polyseed());
return std::string(seed.data(), seed.size()); // TODO
}
+bool WalletImpl::getPolyseed(std::string &seed_words, uint64_t &birthday, bool &is_encrypted) const
+{
+ epee::wipeable_string seed_words_epee;
+ clearStatus();
+
+ if (!m_wallet) {
+ return false;
+ }
+
+ bool result = m_wallet->get_polyseed(seed_words_epee, birthday, is_encrypted);
+
+ seed_words.assign(seed_words_epee.data(), seed_words_epee.size());
+
+ return result;
+}
+
+std::vector<std::pair<std::string, std::string>> Wallet::getPolyseedLanguages()
+{
+ std::vector<std::pair<std::string, std::string>> languages;
+
+ auto langs = polyseed::get_langs();
+ for (const auto &lang : langs) {
+ languages.emplace_back(std::pair<std::string, std::string>(lang.name_en(), lang.name()));
+ }
+
+ return languages;
+}
+
+bool Wallet::createPolyseed(std::string &seed_words, std::string &err, const std::string &language)
+{
+ epee::wipeable_string seed_words_epee(seed_words.c_str(), seed_words.size());
+
+ try {
+ polyseed::data polyseed(POLYSEED_MONERO);
+ const polyseed::language &lang = polyseed::get_lang_by_name(language);
+ if (!lang.valid()) {
+ throw std::runtime_error("invalid Polyseed language");
+ }
+ polyseed.create(0, lang);
+ polyseed.encode(lang, seed_words_epee);
+ seed_words.assign(seed_words_epee.data(), seed_words_epee.size());
+ }
+ catch (const std::exception &e) {
+ err = e.what();
+ return false;
+ }
+
+ return true;
+}
+
std::string WalletImpl::getSeedLanguage() const
{
return m_wallet->get_seed_language();
diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
index 30659cd..a2126ef 100644
--- a/src/wallet/api/wallet.h
+++ b/src/wallet/api/wallet.h
@@ -80,9 +80,16 @@ public:
bool recoverFromDevice(const std::string &path,
const std::string &password,
const std::string &device_name);
+ bool createFromPolyseed(const std::string &path,
+ const std::string &password,
+ const std::string &seed,
+ const std::string &passphrase = "",
+ bool newWallet = true,
+ uint64_t restoreHeight = 0);
Device getDeviceType() const override;
bool close(bool store = true);
std::string seed(const std::string& seed_offset = "") const override;
+ bool getPolyseed(std::string &seed_words, uint64_t& birthday, bool& is_encrypted) const override;
std::string getSeedLanguage() const override;
void setSeedLanguage(const std::string &arg) override;
// void setListener(Listener *) {}
diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
index de13e55..3f599b5 100644
--- a/src/wallet/api/wallet2_api.h
+++ b/src/wallet/api/wallet2_api.h
@@ -715,7 +715,11 @@ struct Wallet
static void warning(const std::string &category, const std::string &str);
static void error(const std::string &category, const std::string &str);
- /**
+ virtual bool getPolyseed(std::string &seed, uint64_t &birthday, bool &is_encrypted) const = 0;
+ static bool createPolyseed(std::string &seed_words, std::string &err, const std::string &language = "English");
+ static std::vector<std::pair<std::string, std::string>> getPolyseedLanguages();
+
+ /**
* @brief StartRefresh - Start/resume refresh thread (refresh every 10 seconds)
*/
virtual void startRefresh() = 0;
@@ -1310,6 +1314,27 @@ struct WalletManager
WalletListener * listener = nullptr) = 0;
/*!
+ * \brief creates a wallet from a Polyseed mnemonic phrase
+ * \param path Name of the wallet file to be created
+ * \param password Password of wallet file
+ * \param nettype Network type
+ * \param mnemonic Polyseed mnemonic
+ * \param passphrase Optional seed offset passphrase
+ * \param newWallet Whether it is a new wallet
+ * \param restoreHeight Override the embedded restore height if recovering
+ * \param kdf_rounds Number of rounds for key derivation function
+ * @return
+ */
+ virtual Wallet * createWalletFromPolyseed(const std::string &path,
+ const std::string &password,
+ NetworkType nettype,
+ const std::string &mnemonic,
+ const std::string &passphrase = "",
+ bool newWallet = true,
+ uint64_t restore_height = 0,
+ uint64_t kdf_rounds = 1) = 0;
+
+/*!
* \brief Closes wallet. In case operation succeeded, wallet object deleted. in case operation failed, wallet object not deleted
* \param wallet previously opened / created wallet instance
* \return None
diff --git a/src/wallet/api/wallet_manager.cpp b/src/wallet/api/wallet_manager.cpp
index b18333e..3c4ce10 100644
--- a/src/wallet/api/wallet_manager.cpp
+++ b/src/wallet/api/wallet_manager.cpp
@@ -152,6 +152,15 @@ Wallet *WalletManagerImpl::createWalletFromDevice(const std::string &path,
return wallet;
}
+Wallet *WalletManagerImpl::createWalletFromPolyseed(const std::string &path, const std::string &password, NetworkType nettype,
+ const std::string &mnemonic, const std::string &passphrase,
+ bool newWallet, uint64_t restoreHeight, uint64_t kdf_rounds)
+{
+ WalletImpl * wallet = new WalletImpl(nettype, kdf_rounds);
+ wallet->createFromPolyseed(path, password, mnemonic, passphrase, newWallet, restoreHeight);
+ return wallet;
+}
+
bool WalletManagerImpl::closeWallet(Wallet *wallet, bool store)
{
WalletImpl * wallet_ = dynamic_cast<WalletImpl*>(wallet);
diff --git a/src/wallet/api/wallet_manager.h b/src/wallet/api/wallet_manager.h
index 45a9f01..e0db454 100644
--- a/src/wallet/api/wallet_manager.h
+++ b/src/wallet/api/wallet_manager.h
@@ -75,6 +75,14 @@ public:
const std::string &subaddressLookahead = "",
uint64_t kdf_rounds = 1,
WalletListener * listener = nullptr) override;
+ Wallet * createWalletFromPolyseed(const std::string &path,
+ const std::string &password,
+ NetworkType nettype,
+ const std::string &mnemonic,
+ const std::string &passphrase,
+ bool newWallet = true,
+ uint64_t restore_height = 0,
+ uint64_t kdf_rounds = 1) override;
virtual bool closeWallet(Wallet *wallet, bool store = true) override;
bool walletExists(const std::string &path) override;
bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key, uint64_t kdf_rounds = 1) const override;
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 44e9727..a1508eb 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -1275,7 +1275,8 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended, std
m_enable_multisig(false),
m_pool_info_query_time(0),
m_has_ever_refreshed_from_node(false),
- m_allow_mismatched_daemon_version(false)
+ m_allow_mismatched_daemon_version(false),
+ m_polyseed(false)
{
}
@@ -1446,7 +1447,7 @@ bool wallet2::is_deterministic() const
return memcmp(second.data,get_account().get_keys().m_view_secret_key.data, sizeof(crypto::secret_key)) == 0;
}
//----------------------------------------------------------------------------------------------------
-bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase) const
+bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase, bool force_english) const
{
bool keys_deterministic = is_deterministic();
if (!keys_deterministic)
@@ -1454,24 +1455,66 @@ bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeab
std::cout << "This is not a deterministic wallet" << std::endl;
return false;
}
- if (seed_language.empty())
+ std::string seed_language_to_use;
+ if (force_english)
{
- std::cout << "seed_language not set" << std::endl;
- return false;
+ seed_language_to_use = "English";
+ }
+ else if (seed_language.empty())
+ {
+ // Don't refuse query anymore as it was done for a decade, but also default to English;
+ // There are important third-party wallet apps around that don't set the seed language
+ // under some circumstances
+ seed_language_to_use = "English";
+ }
+ else
+ {
+ seed_language_to_use = seed_language;
}
crypto::secret_key key = get_account().get_keys().m_spend_secret_key;
if (!passphrase.empty())
key = cryptonote::encrypt_key(key, passphrase);
- if (!crypto::ElectrumWords::bytes_to_words(key, electrum_words, seed_language))
+ if (!crypto::ElectrumWords::bytes_to_words(key, electrum_words, seed_language_to_use))
{
- std::cout << "Failed to create seed from key for language: " << seed_language << std::endl;
+ std::cout << "Failed to create seed from key for language: " << seed_language_to_use << std::endl;
return false;
}
return true;
}
//----------------------------------------------------------------------------------------------------
+bool wallet2::get_polyseed(epee::wipeable_string& polyseed, uint64_t& birthday, bool& is_encrypted) const
+{
+ if (!m_polyseed)
+ {
+ return false;
+ }
+
+ try
+ {
+ polyseed::data data(POLYSEED_MONERO);
+ data.load(get_account().get_keys().m_polyseed);
+ std::string seed_language_to_use;
+ if (seed_language.empty())
+ {
+ seed_language_to_use = "English";
+ }
+ else
+ {
+ seed_language_to_use = seed_language;
+ }
+ data.encode(polyseed::get_lang_by_name(seed_language_to_use), polyseed);
+ birthday = data.birthday();
+ is_encrypted = data.encrypted();
+ }
+ catch (...)
+ {
+ return false;
+ }
+ return true;
+}
+//----------------------------------------------------------------------------------------------------
bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase) const
{
const multisig::multisig_account_status ms_status{get_multisig_status()};
@@ -3023,7 +3066,12 @@ void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::trans
bool wallet2::should_skip_block(const cryptonote::block &b, uint64_t height) const
{
// seeking only for blocks that are not older then the wallet creation time plus 1 day. 1 day is for possible user incorrect time setup
- return !(b.timestamp + 60*60*24 > m_account.get_createtime() && height >= m_refresh_from_block_height && height >= m_skip_to_height);
+ // With a Polyseed we have to ignore the wallet creation time however because checking that would make it impossible to scan with a
+ // block height earlier than the Polyseed birthday, which people might want to do, for whatever reason; without override, that birthday
+ // went into 'm_refresh_from_block_height' already anyway
+ return !((m_polyseed || b.timestamp + 60*60*24 > m_account.get_createtime())
+ && height >= m_refresh_from_block_height
+ && height >= m_skip_to_height);
}
//----------------------------------------------------------------------------------------------------
void wallet2::process_new_blockchain_entry(const cryptonote::block& b, const cryptonote::block_complete_entry& bche, const parsed_block &parsed_block, const crypto::hash& bl_id, uint64_t height, const std::vector<tx_cache_data> &tx_cache_data, size_t tx_cache_data_offset, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache)
@@ -4834,6 +4882,9 @@ boost::optional<wallet2::keys_file_data> wallet2::get_keys_file_data(const crypt
value2.SetInt(m_enable_multisig ? 1 : 0);
json.AddMember("enable_multisig", value2, json.GetAllocator());
+ value2.SetInt(m_polyseed ? 1 : 0);
+ json.AddMember("polyseed", value2, json.GetAllocator());
+
if (m_background_sync_type == BackgroundSyncCustomPassword && !background_keys_file && m_custom_background_key)
{
value.SetString(reinterpret_cast<const char*>(m_custom_background_key.get().data()), m_custom_background_key.get().size());
@@ -5068,6 +5119,7 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
m_enable_multisig = false;
m_allow_mismatched_daemon_version = false;
m_custom_background_key = boost::none;
+ m_polyseed = false;
}
else if(json.IsObject())
{
@@ -5292,6 +5344,8 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, enable_multisig, int, Int, false, false);
m_enable_multisig = field_enable_multisig;
+ GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, polyseed, int, Int, false, false);
+ m_polyseed = field_polyseed;
GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, background_sync_type, BackgroundSyncType, Int, false, BackgroundSyncOff);
m_background_sync_type = field_background_sync_type;
@@ -5413,12 +5467,12 @@ bool wallet2::load_keys_buf(const std::string& keys_buf, const epee::wipeable_st
* can be used prior to rewriting wallet keys file, to ensure user has entered the correct password
*
*/
-bool wallet2::verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out)
+bool wallet2::verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out, cryptonote::account_keys &keys_out)
{
// this temporary unlocking is necessary for Windows (otherwise the file couldn't be loaded).
unlock_keys_file();
const bool no_spend_key = m_account.get_device().device_protocol() == hw::device::PROTOCOL_COLD || m_watch_only || m_multisig || m_is_background_wallet;
- bool r = verify_password(m_keys_file, password, no_spend_key, m_account.get_device(), m_kdf_rounds, spend_key_out);
+ bool r = verify_password(m_keys_file, password, no_spend_key, m_account.get_device(), m_kdf_rounds, spend_key_out, keys_out);
lock_keys_file();
return r;
}
@@ -5436,7 +5490,7 @@ bool wallet2::verify_password(const epee::wipeable_string& password, crypto::sec
* can be used prior to rewriting wallet keys file, to ensure user has entered the correct password
*
*/
-bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds, crypto::secret_key &spend_key_out)
+bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds, crypto::secret_key &spend_key_out, cryptonote::account_keys &keys_out)
{
rapidjson::Document json;
wallet2::keys_file_data keys_file_data;
@@ -5494,6 +5548,7 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip
if(!no_spend_key)
r = r && hwdev.verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key);
spend_key_out = (!no_spend_key && r) ? keys.m_spend_secret_key : crypto::null_skey;
+ keys_out = keys;
return r;
}
@@ -5623,6 +5678,28 @@ void wallet2::init_type(hw::device::device_type device_type)
m_key_device_type = device_type;
}
+void wallet2::prepare_generate(const std::string& wallet_)
+{
+ clear();
+ prepare_file_names(wallet_);
+
+ if (!wallet_.empty())
+ {
+ boost::system::error_code ignored_ec;
+ THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
+ THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
+ }
+}
+
+void wallet2::finish_generate(const std::string& wallet_, bool watch_only, const epee::wipeable_string& password, bool create_address_file)
+{
+ create_keys_file(wallet_, watch_only, password, m_nettype != MAINNET || create_address_file);
+ setup_new_blockchain();
+
+ if (!wallet_.empty())
+ store();
+}
+
/*!
* \brief Generates a wallet or restores one. Assumes the multisig setup
* has already completed for the provided multisig info.
@@ -5634,15 +5711,7 @@ void wallet2::init_type(hw::device::device_type device_type)
void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password,
const epee::wipeable_string& multisig_data, bool create_address_file)
{
- clear();
- prepare_file_names(wallet_);
-
- if (!wallet_.empty())
- {
- boost::system::error_code ignored_ec;
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
- }
+ prepare_generate(wallet_);
m_account.generate(rct::rct2sk(rct::zero()), true, false);
@@ -5709,11 +5778,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
m_multisig_rounds_passed = multisig::multisig_setup_rounds_required(m_multisig_signers.size(), m_multisig_threshold);
setup_keys(password);
- create_keys_file(wallet_, false, password, m_nettype != MAINNET || create_address_file);
- setup_new_blockchain();
-
- if (!wallet_.empty())
- store();
+ finish_generate(wallet_, false, password, create_address_file);
}
/*!
@@ -5729,15 +5794,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password,
const crypto::secret_key& recovery_param, bool recover, bool two_random, bool create_address_file)
{
- clear();
- prepare_file_names(wallet_);
-
- if (!wallet_.empty())
- {
- boost::system::error_code ignored_ec;
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
- }
+ prepare_generate(wallet_);
crypto::secret_key retval = m_account.generate(recovery_param, recover, two_random);
@@ -5749,57 +5806,123 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip
m_refresh_from_block_height = estimate_blockchain_height();
}
- create_keys_file(wallet_, false, password, m_nettype != MAINNET || create_address_file);
+ finish_generate(wallet_, false, password, create_address_file);
- setup_new_blockchain();
+ return retval;
+}
- if (!wallet_.empty())
- store();
+/*!
+* \brief Generates a wallet or restores one from a Polyseed.
+* \param wallet_ Name of wallet file
+* \param password Password of wallet file
+* \param seed Polyseed data
+* \param passphrase Optional seed offset passphrase
+* \param recover Whether it is a restore
+* \param restoreHeight Override the Polyseed's embedded restore height if recovering
+* \param create_address_file Whether to create an address file
+* \return The secret key of the generated wallet
+*/
+crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password,
+ const polyseed::data &seed, const epee::wipeable_string& passphrase, bool recover, uint64_t restoreHeight, bool create_address_file)
+{
+ prepare_generate(wallet_);
- return retval;
+ crypto::secret_key spend_secret_key = seed.generate_secret_key(passphrase);
+ crypto::secret_key polyseed_storage;
+ seed.save(&polyseed_storage);
+ m_account.create_from_polyseed(spend_secret_key, polyseed_storage, seed.birthday());
+
+ init_type(hw::device::device_type::SOFTWARE);
+ m_polyseed = true;
+ setup_keys(password);
+
+ if (recover) {
+ m_refresh_from_block_height = restoreHeight > 0 ? restoreHeight : estimate_blockchain_height(seed.birthday());
+ }
+ else {
+ if (m_refresh_from_block_height == 0) {
+ m_refresh_from_block_height = estimate_blockchain_height();
+ }
+ }
+
+ finish_generate(wallet_, false, password, create_address_file);
+
+ return spend_secret_key;
}
- uint64_t wallet2::estimate_blockchain_height()
- {
- // -1 month for fluctuations in block time and machine date/time setup.
- // avg seconds per block
- const int seconds_per_block = DIFFICULTY_TARGET_V2;
- // ~num blocks per month
- const uint64_t blocks_per_month = 60*60*24*30/seconds_per_block;
-
- // try asking the daemon first
- std::string err;
- uint64_t height = 0;
-
- // we get the max of approximated height and local height.
- // approximated height is the least of daemon target height
- // (the max of what the other daemons are claiming is their
- // height) and the theoretical height based on the local
- // clock. This will be wrong only if both the local clock
- // is bad *and* a peer daemon claims a highest height than
- // the real chain.
- // local height is the height the local daemon is currently
- // synced to, it will be lower than the real chain height if
- // the daemon is currently syncing.
- // If we use the approximate height we subtract one month as
- // a safety margin.
- height = get_approximate_blockchain_height();
- uint64_t target_height = get_daemon_blockchain_target_height(err);
- if (err.empty()) {
- if (target_height < height)
- height = target_height;
- } else {
- // if we couldn't talk to the daemon, check safety margin.
- if (height > blocks_per_month)
- height -= blocks_per_month;
- else
- height = 0;
- }
- uint64_t local_height = get_daemon_blockchain_height(err);
- if (err.empty() && local_height > height)
- height = local_height;
- return height;
- }
+/*!
+* \brief Estimate current blockchain height, or blockchain height at a given time
+* \param time If not 0, ask for blockchain height at a given time
+* \return Blockchain height estimated as best as possible
+*/
+uint64_t wallet2::estimate_blockchain_height(uint64_t time)
+{
+ // -1 month for fluctuations in block time and machine date/time setup.
+ // avg seconds per block
+ const int seconds_per_block = DIFFICULTY_TARGET_V2;
+ // ~num blocks per month
+ const uint64_t blocks_per_month = 60*60*24*30/seconds_per_block;
+
+ std::string err;
+ uint64_t height = 0;
+
+ height = get_approximate_blockchain_height(time);
+ if (time == 0) {
+ // we get the max of approximated height and local height.
+ // approximated height is the least of daemon target height
+ // (the max of what the other daemons are claiming is their
+ // height) and the theoretical height based on the local
+ // clock. This will be wrong only if both the local clock
+ // is bad *and* a peer daemon claims a highest height than
+ // the real chain.
+ // local height is the height the local daemon is currently
+ // synced to, it will be lower than the real chain height if
+ // the daemon is currently syncing.
+ // If we use the approximate height we subtract one month as
+ // a safety margin.
+ uint64_t target_height = get_daemon_blockchain_target_height(err);
+ if (err.empty()) {
+ if (target_height < height)
+ height = target_height;
+ } else {
+ // if we couldn't talk to the daemon, check safety margin.
+ if (height > blocks_per_month)
+ height -= blocks_per_month;
+ else
+ height = 0;
+ }
+ uint64_t local_height = get_daemon_blockchain_height(err);
+ if (err.empty() && local_height > height)
+ height = local_height;
+ }
+
+ else {
+ // Try to ask the daemon, if connected it will know better than our approximation
+ uint64_t daemon_height = 0;
+ if (m_is_initialized && !m_offline) {
+ try {
+ daemon_height = get_blockchain_height_by_timestamp(time);
+ }
+ catch (const std::runtime_error& e) {
+ }
+ }
+ if (daemon_height != 0 && daemon_height < height) {
+ // Only taking a daemon height lower than our estimate should offer basic protection
+ // against malicious nodes that report us a height that is too high and would cause
+ // us missing blocks
+ height = daemon_height;
+ }
+ else {
+ // Subtract safety margin from the approximation
+ if (height > blocks_per_month)
+ height -= blocks_per_month;
+ else
+ height = 0;
+ }
+ }
+
+ return height;
+}
/*!
* \brief Creates a watch only wallet from a public address and a view secret key.
@@ -5813,15 +5936,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
const cryptonote::account_public_address &account_public_address,
const crypto::secret_key& viewkey, bool create_address_file)
{
- clear();
- prepare_file_names(wallet_);
-
- if (!wallet_.empty())
- {
- boost::system::error_code ignored_ec;
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
- }
+ prepare_generate(wallet_);
m_account.create_from_viewkey(account_public_address, viewkey);
init_type(hw::device::device_type::SOFTWARE);
@@ -5829,12 +5944,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
m_account_public_address = account_public_address;
setup_keys(password);
- create_keys_file(wallet_, true, password, m_nettype != MAINNET || create_address_file);
-
- setup_new_blockchain();
-
- if (!wallet_.empty())
- store();
+ finish_generate(wallet_, true, password, create_address_file);
}
/*!
@@ -5850,27 +5960,14 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
const cryptonote::account_public_address &account_public_address,
const crypto::secret_key& spendkey, const crypto::secret_key& viewkey, bool create_address_file)
{
- clear();
- prepare_file_names(wallet_);
-
- if (!wallet_.empty())
- {
- boost::system::error_code ignored_ec;
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
- }
+ prepare_generate(wallet_);
m_account.create_from_keys(account_public_address, spendkey, viewkey);
init_type(hw::device::device_type::SOFTWARE);
m_account_public_address = account_public_address;
setup_keys(password);
- create_keys_file(wallet_, false, password, create_address_file);
-
- setup_new_blockchain();
-
- if (!wallet_.empty())
- store();
+ finish_generate(wallet_, false, password, create_address_file);
}
/*!
@@ -5881,14 +5978,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string&
*/
void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& password, const std::string &device_name, bool create_address_file)
{
- clear();
- prepare_file_names(wallet_);
-
- boost::system::error_code ignored_ec;
- if (!wallet_.empty()) {
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file);
- THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file);
- }
+ prepare_generate(wallet_);
auto &hwdev = lookup_device(device_name);
hwdev.set_name(device_name);
@@ -5901,17 +5991,14 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p
setup_keys(password);
m_device_name = device_name;
- create_keys_file(wallet_, false, password, m_nettype != MAINNET || create_address_file);
if (m_subaddress_lookahead_major == SUBADDRESS_LOOKAHEAD_MAJOR && m_subaddress_lookahead_minor == SUBADDRESS_LOOKAHEAD_MINOR)
{
// the default lookahead setting (50:200) is clearly too much for hardware wallet
m_subaddress_lookahead_major = 5;
m_subaddress_lookahead_minor = 20;
}
- setup_new_blockchain();
- if (!wallet_.empty()) {
- store();
- }
+
+ finish_generate(wallet_, false, password, create_address_file);
}
//----------------------------------------------------------------------------------------------------
void wallet2::get_uninitialized_multisig_account(multisig::multisig_account &account_out) const
@@ -6025,6 +6112,7 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password,
m_multisig_threshold = threshold;
m_multisig_signers = signers;
m_multisig_rounds_passed = 1;
+ m_polyseed = false; // but let the stored Polyseed itself stand, maybe somebody will have a use for that
// derivations stored (note: should be empty in last kex round)
m_multisig_derivations.clear();
@@ -12912,7 +13000,7 @@ uint64_t wallet2::get_daemon_blockchain_target_height(string &err)
return target_height;
}
-uint64_t wallet2::get_approximate_blockchain_height() const
+uint64_t wallet2::get_approximate_blockchain_height(uint64_t t) const
{
const size_t wallet_num_hard_forks = m_nettype == TESTNET ? num_testnet_hard_forks
: m_nettype == STAGENET ? num_stagenet_hard_forks
@@ -12928,7 +13016,7 @@ uint64_t wallet2::get_approximate_blockchain_height() const
const int seconds_per_block = DIFFICULTY_TARGET_V2;
// Calculated blockchain height
uint64_t approx_blockchain_height = fork_block;
- const time_t now = time(NULL);
+ const time_t now = t > 0 ? t : time(NULL);
if (now > fork_time)
{
const uint64_t blocks_since_last_fork = static_cast<uint64_t>((now - fork_time) / seconds_per_block);
@@ -14021,6 +14109,10 @@ void wallet2::start_background_sync()
return;
}
+ THROW_WALLET_EXCEPTION_IF(m_polyseed && m_wallet_file.empty(), error::wallet_internal_error,
+ "Cannot background sync an in-memory Polyseed wallet");
+ // Because at stopping we will have to read the Polyseed out of the file to restore it
+
if (m_background_sync_type == BackgroundSyncCustomPassword && !m_wallet_file.empty())
{
// Save the current state of the wallet cache. Only necessary when using a
@@ -14053,9 +14145,10 @@ void wallet2::stop_background_sync(const epee::wipeable_string &wallet_password,
// Verify provided password and spend secret key. If no spend secret key is
// provided, recover it from the wallet keys file
crypto::secret_key recovered_spend_key = crypto::null_skey;
+ cryptonote::account_keys recovered_keys;
if (!m_wallet_file.empty())
{
- THROW_WALLET_EXCEPTION_IF(!verify_password(wallet_password, recovered_spend_key), error::invalid_password);
+ THROW_WALLET_EXCEPTION_IF(!verify_password(wallet_password, recovered_spend_key, recovered_keys), error::invalid_password);
}
else
{
@@ -14098,11 +14191,20 @@ void wallet2::stop_background_sync(const epee::wipeable_string &wallet_password,
// Reload the wallet from disk
load(m_wallet_file, wallet_password);
THROW_WALLET_EXCEPTION_IF(!verify_spend_key(recovered_spend_key), error::invalid_spend_key);
+ if (is_key_encryption_enabled())
+ this->decrypt_keys(wallet_password);
}
m_background_syncing = false;
// Set the plaintext spend key
m_account.set_spend_key(recovered_spend_key);
+
+ // Restore m_polyseed
+ if (m_polyseed && m_background_sync_type == BackgroundSyncReusePassword && !m_wallet_file.empty())
+ {
+ m_account.set_polyseed(recovered_keys.m_polyseed);
+ }
+
if (is_key_encryption_enabled())
this->encrypt_keys(wallet_password);
@@ -15172,15 +15274,6 @@ bool wallet2::parse_uri_impl(const std::string &uri, const cryptonote::network_t
//----------------------------------------------------------------------------------------------------
uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day)
{
- uint32_t version;
- if (!check_connection(&version))
- {
- throw std::runtime_error("failed to connect to daemon: " + get_daemon_address());
- }
- if (version < MAKE_CORE_RPC_VERSION(1, 6))
- {
- throw std::runtime_error("this function requires RPC version 1.6 or higher");
- }
std::tm date = { 0, 0, 0, 0, 0, 0, 0, 0 };
date.tm_year = year - 1900;
date.tm_mon = month - 1;
@@ -15189,14 +15282,31 @@ uint64_t wallet2::get_blockchain_height_by_date(uint16_t year, uint8_t month, ui
{
throw std::runtime_error("month or day out of range");
}
+
uint64_t timestamp_target = std::mktime(&date);
+
+ return get_blockchain_height_by_timestamp(timestamp_target);
+}
+//----------------------------------------------------------------------------------------------------
+uint64_t wallet2::get_blockchain_height_by_timestamp(uint64_t timestamp_target) {
+ uint32_t version;
+ if (!check_connection(&version))
+ {
+ throw std::runtime_error("failed to connect to daemon: " + get_daemon_address());
+ }
+ if (version < MAKE_CORE_RPC_VERSION(1, 6))
+ {
+ throw std::runtime_error("this function requires RPC version 1.6 or higher");
+ }
+
std::string err;
uint64_t height_min = 0;
- uint64_t height_max = get_daemon_blockchain_height(err) - 1;
- if (!err.empty())
+ uint64_t height_max = get_daemon_blockchain_height(err);
+ if (height_max == 0 || !err.empty())
{
throw std::runtime_error("failed to get blockchain height");
}
+ height_max--;
while (true)
{
COMMAND_RPC_GET_BLOCKS_BY_HEIGHT::request req;
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index 506561a..c784f79 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -68,6 +68,7 @@
#include "fee_priority.h"
#include "fee_algorithm.h"
#include "wallet2_basic/wallet2_types.h"
+#include "mnemonics/polyseed/polyseed.hpp"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2"
@@ -237,9 +238,10 @@ private:
static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds)
{
crypto::secret_key spend_key = crypto::null_skey;
- return verify_password(keys_file_name, password, no_spend_key, hwdev, kdf_rounds, spend_key);
+ cryptonote::account_keys keys;
+ return verify_password(keys_file_name, password, no_spend_key, hwdev, kdf_rounds, spend_key, keys);
};
- static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds, crypto::secret_key &spend_key_out);
+ static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key, hw::device &hwdev, uint64_t kdf_rounds, crypto::secret_key &spend_key_out, cryptonote::account_keys &keys_out);
static bool query_device(hw::device::device_type& device_type, const std::string& keys_file_name, const epee::wipeable_string& password, uint64_t kdf_rounds = 1);
wallet2(cryptonote::network_type nettype = cryptonote::MAINNET, uint64_t kdf_rounds = 1, bool unattended = false, std::unique_ptr<epee::net_utils::http::http_client_factory> http_client_factory = std::unique_ptr<epee::net_utils::http::http_client_factory>(new net::http::client_factory()));
@@ -608,6 +610,10 @@ private:
tx_entry_data(): lowest_height((uint64_t)-1), highest_height(0) {}
};
+ // Helpers for a number of wallet generate calls to reduce code duplication
+ void prepare_generate(const std::string& wallet_);
+ void finish_generate(const std::string& wallet_, bool watch_only, const epee::wipeable_string& password, bool create_address_file);
+
/*!
* \brief Generates a wallet or restores one. Assumes the multisig setup
* has already completed for the provided multisig info.
@@ -632,7 +638,23 @@ private:
crypto::secret_key generate(const std::string& wallet, const epee::wipeable_string& password,
const crypto::secret_key& recovery_param = crypto::secret_key(), bool recover = false,
bool two_random = false, bool create_address_file = false);
+
/*!
+ * \brief Generates a wallet or restores one from a Polyseed.
+ * \param wallet_ Name of wallet file
+ * \param password Password of wallet file
+ * \param seed Polyseed data
+ * \param passphrase Optional seed offset passphrase
+ * \param recover Whether it is a restore
+ * \param restoreHeight Override the Polyseed's embedded restore height
+ * \param create_address_file Whether to create an address file
+ * \return The secret key of the generated wallet
+ */
+ crypto::secret_key generate(const std::string& wallet_, const epee::wipeable_string& password,
+ const polyseed::data &seed, const epee::wipeable_string& passphrase = "",
+ bool recover = false, uint64_t restoreHeight = 0, bool create_address_file = false);
+
+ /*!
* \brief Creates a wallet from a public address and a spend/view secret key pair.
* \param wallet_ Name of wallet file
* \param password Password of wallet file
@@ -766,8 +788,8 @@ private:
/*!
* \brief verifies given password is correct for default wallet keys file
*/
- bool verify_password(const epee::wipeable_string& password) {crypto::secret_key key = crypto::null_skey; return verify_password(password, key);};
- bool verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out);
+ bool verify_password(const epee::wipeable_string& password) {crypto::secret_key spend_key = crypto::null_skey; cryptonote::account_keys keys; return verify_password(password, spend_key, keys);};
+ bool verify_password(const epee::wipeable_string& password, crypto::secret_key &spend_key_out, cryptonote::account_keys &keys_out);
cryptonote::account_base& get_account(){return m_account;}
const cryptonote::account_base& get_account()const{return m_account;}
@@ -813,7 +835,10 @@ private:
* \brief Checks if deterministic wallet
*/
bool is_deterministic() const;
- bool get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase = epee::wipeable_string()) const;
+ bool is_polyseed() const { return m_polyseed; }
+ void set_is_polyseed(bool is_polyseed) { m_polyseed = is_polyseed; }
+ bool get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase = epee::wipeable_string(), bool force_english = false) const;
+ bool get_polyseed(epee::wipeable_string& polyseed, uint64_t& birthday, bool& is_encrypted) const;
/*!
* \brief Gets the seed language
@@ -1272,8 +1297,8 @@ private:
/*!
* \brief Calculates the approximate blockchain height from current date/time.
*/
- uint64_t get_approximate_blockchain_height() const;
- uint64_t estimate_blockchain_height();
+ uint64_t get_approximate_blockchain_height(uint64_t time = 0) const;
+ uint64_t estimate_blockchain_height(uint64_t time = 0);
std::vector<size_t> select_available_outputs_from_histogram(uint64_t count, bool atleast, bool unlocked, bool allow_rct);
std::vector<size_t> select_available_outputs(const std::function<bool(const transfer_details &td)> &f);
std::vector<size_t> select_available_unmixable_outputs();
@@ -1367,6 +1392,7 @@ private:
static bool parse_uri_impl(const std::string &uri, const cryptonote::network_type, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error);
uint64_t get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day); // 1<=month<=12, 1<=day<=31
+ uint64_t get_blockchain_height_by_timestamp(uint64_t timestamp);
bool is_synced();
@@ -1676,9 +1702,10 @@ private:
hw::device::device_type m_key_device_type;
cryptonote::network_type m_nettype;
uint64_t m_kdf_rounds;
- std::string seed_language; /*!< Language of the mnemonics (seed). */
+ std::string seed_language; /*!< Language of the mnemonics (seed), in that language, e.g. "Deutsch" for German */
bool is_old_file_format; /*!< Whether the wallet file is of an old file format */
bool m_watch_only; /*!< no spend key */
+ bool m_polyseed;
bool m_multisig; /*!< if > 1 spend secret key will not match spend public key */
uint32_t m_multisig_threshold;
std::vector<crypto::public_key> m_multisig_signers;
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 71e47d1..d6bc993 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -2312,6 +2312,9 @@ namespace tools
}
if (!m_wallet) return not_open(er);
+ res.polyseed_birthday = 0;
+ res.polyseed_is_encrypted = false;
+
if (req.key_type.compare("mnemonic") == 0)
{
epee::wipeable_string seed;
@@ -2347,7 +2350,16 @@ namespace tools
er.message = "The wallet is non-deterministic. Cannot display seed.";
return false;
}
- if (!m_wallet->get_seed(seed))
+ bool got_seed;
+ if (m_wallet->is_polyseed())
+ {
+ got_seed = m_wallet->get_polyseed(seed, res.polyseed_birthday, res.polyseed_is_encrypted);
+ }
+ else
+ {
+ got_seed = m_wallet->get_seed(seed);
+ }
+ if (!got_seed)
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Failed to get seed.";
@@ -2450,20 +2462,32 @@ namespace tools
{
crypto::secret_key recovery_key;
std::string language;
+ bool is_polyseed = false;
+ polyseed::data polyseed(POLYSEED_MONERO);
- if (!crypto::ElectrumWords::words_to_bytes(req.seed, recovery_key, language))
+ if (!crypto::ElectrumWords::words_to_bytes_ex(req.seed, recovery_key, language, is_polyseed, polyseed))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Electrum-style word list failed verification";
return false;
}
- if (!req.seed_offset.empty())
+ if (!req.seed_offset.empty() && !is_polyseed)
recovery_key = cryptonote::decrypt_key(recovery_key, req.seed_offset);
// generate spend key
cryptonote::account_base account;
- account.generate(recovery_key, true, false);
+ if (is_polyseed)
+ {
+ crypto::secret_key spend_secret_key = polyseed.generate_secret_key(req.seed_offset);
+ crypto::secret_key polyseed_storage;
+ polyseed.save(&polyseed_storage);
+ account.create_from_polyseed(spend_secret_key, polyseed_storage, polyseed.birthday());
+ }
+ else
+ {
+ account.generate(recovery_key, true, false);
+ }
spend_secret_key = account.get_keys().m_spend_secret_key;
}
@@ -3586,8 +3610,8 @@ namespace tools
//------------------------------------------------------------------------------------------------------------------------------
bool wallet_rpc_server::on_get_languages(const wallet_rpc::COMMAND_RPC_GET_LANGUAGES::request& req, wallet_rpc::COMMAND_RPC_GET_LANGUAGES::response& res, epee::json_rpc::error& er, const connection_context *ctx)
{
- crypto::ElectrumWords::get_language_list(res.languages, true);
- crypto::ElectrumWords::get_language_list(res.languages_local, false);
+ crypto::ElectrumWords::get_language_list(res.languages, true, req.polyseed);
+ crypto::ElectrumWords::get_language_list(res.languages_local, false, req.polyseed);
return true;
}
//------------------------------------------------------------------------------------------------------------------------------
@@ -3623,7 +3647,7 @@ namespace tools
}
std::string wallet_file = req.filename.empty() ? "" : (m_wallet_dir + "/" + req.filename);
{
- if (!crypto::ElectrumWords::is_valid_language(req.language))
+ if (!crypto::ElectrumWords::is_valid_language(req.language, req.polyseed))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Unknown language: " + req.language;
@@ -3651,15 +3675,27 @@ namespace tools
return false;
}
wal->set_seed_language(req.language);
- cryptonote::COMMAND_RPC_GET_HEIGHT::request hreq;
- cryptonote::COMMAND_RPC_GET_HEIGHT::response hres;
- hres.height = 0;
- bool r = wal->invoke_http_json("/getheight", hreq, hres);
- if (r)
- wal->set_refresh_from_block_height(hres.height);
+ if (!req.polyseed)
+ {
+ cryptonote::COMMAND_RPC_GET_HEIGHT::request hreq;
+ cryptonote::COMMAND_RPC_GET_HEIGHT::response hres;
+ hres.height = 0;
+ bool r = wal->invoke_http_json("/getheight", hreq, hres);
+ if (r)
+ wal->set_refresh_from_block_height(hres.height);
+ }
crypto::secret_key dummy_key;
try {
- wal->generate(wallet_file, req.password, dummy_key, false, false);
+ if (req.polyseed)
+ {
+ polyseed::data polyseed(POLYSEED_MONERO);
+ polyseed.create(0, polyseed::get_lang_by_name(req.language));
+ wal->generate(wallet_file, req.password, polyseed);
+ }
+ else
+ {
+ wal->generate(wallet_file, req.password, dummy_key, false, false);
+ }
}
catch (const std::exception& e)
{
@@ -3961,7 +3997,7 @@ namespace tools
return false;
}
}
- if (!req.language.empty() && !crypto::ElectrumWords::is_valid_language(req.language))
+ if (!req.language.empty() && !crypto::ElectrumWords::is_valid_language(req.language, false))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "The specified seed language is invalid.";
@@ -4153,10 +4189,12 @@ namespace tools
}
crypto::secret_key recovery_key;
std::string old_language;
+ bool is_polyseed = false;
+ polyseed::data polyseed(POLYSEED_MONERO);
// check the given seed
if (!req.enable_multisig_experimental) {
- if (!crypto::ElectrumWords::words_to_bytes(req.seed, recovery_key, old_language))
+ if (!crypto::ElectrumWords::words_to_bytes_ex(req.seed, recovery_key, old_language, is_polyseed, polyseed))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Electrum-style word list failed verification";
@@ -4185,7 +4223,7 @@ namespace tools
return false;
}
- if (!req.seed_offset.empty())
+ if (!req.seed_offset.empty() && !is_polyseed)
{
recovery_key = cryptonote::decrypt_key(recovery_key, req.seed_offset);
}
@@ -4216,8 +4254,9 @@ namespace tools
epee::wipeable_string password = rc.second.password();
- bool was_deprecated_wallet = ((old_language == crypto::ElectrumWords::old_language_name) ||
- crypto::ElectrumWords::get_is_old_style_seed(req.seed));
+ bool was_deprecated_wallet = (!is_polyseed &&
+ ((old_language == crypto::ElectrumWords::old_language_name) ||
+ crypto::ElectrumWords::get_is_old_style_seed(req.seed)));
std::string mnemonic_language = old_language;
if (was_deprecated_wallet)
@@ -4234,7 +4273,7 @@ namespace tools
er.message = "Wallet was using the old seed language. You need to specify a new seed language.";
return false;
}
- if (!crypto::ElectrumWords::is_valid_language(req.language))
+ if (!crypto::ElectrumWords::is_valid_language(req.language, is_polyseed))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Wallet was using the old seed language, and the specified new seed language is invalid.";
@@ -4246,6 +4285,7 @@ namespace tools
wal->set_seed_language(mnemonic_language);
crypto::secret_key recovery_val;
+ std::string additional_info = "";
try
{
if (req.enable_multisig_experimental)
@@ -4264,9 +4304,36 @@ namespace tools
wal->generate(wallet_file, std::move(rc.second).password(), multisig_data, false);
wal->enable_multisig(true);
}
+ else if (is_polyseed)
+ {
+ // Generate normal wallet with Polyseed
+ uint64_t restore_height = req.restore_height;
+ uint64_t polyseed_restore_height = 0;
+ try
+ {
+ polyseed_restore_height = wal->estimate_blockchain_height(polyseed.birthday());
+ }
+ catch (const std::runtime_error& e)
+ {
+ }
+ if (polyseed_restore_height != 0)
+ {
+ if (restore_height == 0)
+ {
+ restore_height = polyseed_restore_height;
+ }
+ else
+ {
+ additional_info = (boost::format(tr("Restore height %u from request overwrites restore height %u from Polyseed birthday"))
+ % restore_height % polyseed_restore_height).str();
+ LOG_PRINT_L0(additional_info);
+ }
+ }
+ recovery_val = wal->generate(wallet_file, std::move(rc.second).password(), polyseed, req.seed_offset, true, restore_height, false);
+ }
else
{
- // Generate normal wallet
+ // Generate normal wallet with legacy seed
recovery_val = wal->generate(wallet_file, std::move(rc.second).password(), recovery_key, true, false, false);
}
MINFO("Wallet has been restored.\n");
@@ -4277,8 +4344,16 @@ namespace tools
return false;
}
- // // Convert the secret key back to seed
+ // Convert the secret key back to seed; with Polyseed we give back the English 25 word legacy seed,
+ // like we do with the CLI wallet app 'legacy_seed' command, to have "maximum compatibility"; if
+ // a seed offset passphrase was used it's even impossible to return a Polyseed that would result in
+ // the given spend private key without passphrase. (There is no RPC call "get legacy seed for a
+ // Polyseed".)
epee::wipeable_string electrum_words;
+ if (is_polyseed)
+ {
+ mnemonic_language = "English";
+ }
if (!req.enable_multisig_experimental && !crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
@@ -4295,20 +4370,27 @@ namespace tools
}
// set blockheight if given
- try
- {
- wal->set_refresh_from_block_height(req.restore_height);
- wal->rewrite(wallet_file, password);
- }
- catch (const std::exception &e)
+ if (!is_polyseed)
{
- handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
- return false;
+ try
+ {
+ wal->set_refresh_from_block_height(req.restore_height);
+ wal->rewrite(wallet_file, password);
+ }
+ catch (const std::exception &e)
+ {
+ handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
+ return false;
+ }
}
set_wallet(wal.release());
res.address = m_wallet->get_account().get_public_address_str(m_wallet->nettype());
res.info = "Wallet has been restored successfully.";
+ if (additional_info.length() > 0)
+ {
+ res.info += " " + additional_info;
+ }
return true;
}
//------------------------------------------------------------------------------------------------------------------------------
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index 8a80dc2..87384cf 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -47,7 +47,7 @@
// advance which version they will stop working with
// Don't go over 32767 for any of these
#define WALLET_RPC_VERSION_MAJOR 1
-#define WALLET_RPC_VERSION_MINOR 33
+#define WALLET_RPC_VERSION_MINOR 34
#define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor))
#define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR)
namespace tools
@@ -1138,9 +1138,13 @@ namespace wallet_rpc
struct response_t
{
std::string key;
+ uint64_t polyseed_birthday;
+ bool polyseed_is_encrypted;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(key)
+ KV_SERIALIZE(polyseed_birthday)
+ KV_SERIALIZE(polyseed_is_encrypted)
END_KV_SERIALIZE_MAP()
};
typedef epee::misc_utils::struct_init<response_t> response;
@@ -2169,7 +2173,12 @@ namespace wallet_rpc
{
struct request_t
{
+ bool polyseed;
+
BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE_OPT(polyseed, true)
+ // The "aggressive" / forward looking default of true instead of conservative false is deliberate,
+ // callers who still want legacy languages have to adapt
END_KV_SERIALIZE_MAP()
};
typedef epee::misc_utils::struct_init<request_t> request;
@@ -2194,11 +2203,13 @@ namespace wallet_rpc
std::string filename;
std::string password;
std::string language;
+ bool polyseed;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(filename)
KV_SERIALIZE(password)
KV_SERIALIZE(language)
+ KV_SERIALIZE_OPT(polyseed, true)
END_KV_SERIALIZE_MAP()
};
typedef epee::misc_utils::struct_init<request_t> request;
diff --git a/tests/functional_tests/wallet.py b/tests/functional_tests/wallet.py
index 0b802cf..04b5ef7 100755
--- a/tests/functional_tests/wallet.py
+++ b/tests/functional_tests/wallet.py
@@ -62,11 +62,21 @@ class WalletTest():
daemon.flush_txpool()
def create(self):
- print('Creating wallet')
+ print('Creating Polyseed wallet')
wallet = Wallet()
# close the wallet if any, will throw if none is loaded
try: wallet.close_wallet()
except: pass
+ seed = 'pulse tone truth head invite orphan sock wet crumble oven price corn pilot antenna luxury strategy'
+ res = wallet.restore_deterministic_wallet(seed = seed)
+ assert res.address == '455jFA8HBVzH6nMt2AnNXGR77VR3BypYJXYiYCRkHmTY6XtqebWpu9RhA8gv6q68fC9cuSg2NUX49Wtgtr9Az5ynGgogCzt'
+ # Don't check the returned legacy seed against the Polyseed, it's of course different
+ wallet.close_wallet()
+
+ print('Creating legacy seed wallet')
+ wallet = Wallet()
+ try: wallet.close_wallet()
+ except: pass
seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'
res = wallet.restore_deterministic_wallet(seed = seed)
assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
@@ -304,13 +314,25 @@ class WalletTest():
def languages(self):
print('Testing languages')
wallet = Wallet()
- res = wallet.get_languages()
+
+ # Legacy languages
+ res = wallet.get_languages(polyseed = False)
assert 'English' in res.languages
assert 'English' in res.languages_local
assert 'Dutch' in res.languages
assert 'Nederlands' in res.languages_local
assert 'Japanese' in res.languages
assert u'日本語' in res.languages_local
+
+ # Polyseed languages
+ res = wallet.get_languages()
+ assert 'English' in res.languages
+ assert 'English' in res.languages_local
+ assert 'Spanish' in res.languages
+ assert 'español' in res.languages_local
+ assert 'Japanese' in res.languages
+ assert u'日本語' in res.languages_local
+
try: wallet.close_wallet()
except: pass
languages = res.languages
diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py
index c742299..773e275 100644
--- a/utils/python-rpc/framework/wallet.py
+++ b/utils/python-rpc/framework/wallet.py
@@ -775,10 +775,11 @@ class Wallet(object):
}
return self.rpc.send_json_rpc_request(relay_tx)
- def get_languages(self):
+ def get_languages(self, polyseed = True):
get_languages = {
'method': 'get_languages',
'params': {
+ 'polyseed': polyseed,
},
'jsonrpc': '2.0',
'id': '0'
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.