Seed: refactor

This commit is contained in:
tobtoht 2022-02-25 16:29:33 +01:00
parent f8bc335720
commit c6264ed3a0
No known key found for this signature in database
GPG key ID: 1CADD27F41F45C3C
13 changed files with 869 additions and 695 deletions

View file

@ -204,7 +204,7 @@ bool WindowManager::autoOpenWallet() {
// ######################## WALLET CREATION ########################
void WindowManager::tryCreateWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedLanguage,
void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage,
const QString &seedOffset) {
if(Utils::fileExists(path)) {
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
@ -218,12 +218,12 @@ void WindowManager::tryCreateWallet(FeatherSeed seed, const QString &path, const
}
Wallet *wallet = nullptr;
if (seed.seedType == SeedType::TEVADOR) {
if (seed.type == Seed::Type::TEVADOR) {
wallet = m_walletManager->createDeterministicWalletFromSpendKey(path, password, seed.language, constants::networkType, seed.spendKey, seed.restoreHeight, constants::kdfRounds, seedOffset);
wallet->setCacheAttribute("feather.seed", seed.mnemonic.join(" "));
wallet->setCacheAttribute("feather.seedoffset", seedOffset);
}
if (seed.seedType == SeedType::MONERO) {
if (seed.type == Seed::Type::MONERO) {
wallet = m_walletManager->recoveryWallet(path, password, seed.mnemonic.join(" "), seedOffset, constants::networkType, seed.restoreHeight, constants::kdfRounds);
}

View file

@ -45,7 +45,7 @@ private slots:
void onWalletPassphraseNeeded(bool on_device);
private:
void tryCreateWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset);
void tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset);
void tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight);
void tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight);

View file

@ -9,7 +9,7 @@
#include "utils/os/whonix.h"
#include "utils/networking.h"
#include "utils/FeatherSeed.h"
#include "utils/Seed.h"
#include "utils/daemonrpc.h"
#include "utils/RestoreHeightLookup.h"
#include "utils/nodes.h"

View file

@ -1,117 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2020-2022 The Monero Project
#ifndef FEATHER_FEATHERSEED_H
#define FEATHER_FEATHERSEED_H
#include "libwalletqt/WalletManager.h"
#include "libwalletqt/Wallet.h"
#include "utils/AppData.h"
#include <sstream>
#include "RestoreHeightLookup.h"
enum SeedType {
MONERO = 0, // 25 word seeds
TEVADOR // 14 word seeds
};
struct FeatherSeed {
// TODO: this is spaghetti, needs refactor
NetworkType::Type netType;
QString coin;
QString language;
SeedType seedType;
QStringList mnemonic;
QString spendKey;
QString correction;
time_t time;
int restoreHeight = 0;
QString errorString;
explicit FeatherSeed(NetworkType::Type networkType = NetworkType::MAINNET,
const QString &coin = "monero",
const QString &language = "English",
const QStringList &mnemonic = {})
: netType(networkType), coin(coin), language(language), mnemonic(mnemonic)
{
// Generate a new mnemonic if none was given
if (mnemonic.length() == 0) {
this->time = std::time(nullptr);
monero_seed seed(this->time, coin.toStdString());
std::stringstream buffer;
buffer << seed;
this->mnemonic = QString::fromStdString(buffer.str()).split(" ");
buffer.str(std::string());
buffer << seed.key();
this->spendKey = QString::fromStdString(buffer.str());
this->setRestoreHeight();
}
if (mnemonic.length() == 25) {
this->seedType = SeedType::MONERO;
}
else if (mnemonic.length() == 14) {
this->seedType = SeedType::TEVADOR;
} else {
this->errorString = "Mnemonic seed does not match known type";
return;
}
if (seedType == SeedType::TEVADOR) {
try {
monero_seed seed(mnemonic.join(" ").toStdString(), coin.toStdString());
this->time = seed.date();
this->setRestoreHeight();
std::stringstream buffer;
buffer << seed.key();
this->spendKey = QString::fromStdString(buffer.str());
this->correction = QString::fromStdString(seed.correction());
if (!this->correction.isEmpty()) {
buffer.str(std::string());
buffer << seed;
int index = this->mnemonic.indexOf("xxxx");
this->mnemonic.replace(index, this->correction);
}
}
catch (const std::exception &e) {
this->errorString = e.what();
return;
}
}
}
void setRestoreHeight() {
if (this->time == 0)
this->restoreHeight = 1;
this->restoreHeight = appData()->restoreHeights[netType]->dateToHeight(this->time);
}
int setRestoreHeight(int height) {
auto now = std::time(nullptr);
auto nowClearance = 3600 * 24;
auto currentBlockHeight = appData()->restoreHeights[netType]->dateToHeight(now - nowClearance);
if (height >= currentBlockHeight + nowClearance) {
qCritical() << "unrealistic restore height detected, setting to current blockheight instead: " << currentBlockHeight;
this->restoreHeight = currentBlockHeight;
} else {
this->restoreHeight = height;
}
return this->restoreHeight;
}
};
#endif //FEATHER_FEATHERSEED_H

View file

@ -14,10 +14,10 @@
struct RestoreHeightLookup {
NetworkType::Type type;
QMap<int, int> data;
QMap<time_t, int> data;
explicit RestoreHeightLookup(NetworkType::Type type) : type(type) {}
int dateToHeight(int date) {
int dateToHeight(time_t date) {
// restore height based on a given timestamp using a lookup
// table. If it cannot find the date in the lookup table, it
// will calculate the blockheight based off the last known
@ -28,34 +28,43 @@ struct RestoreHeightLookup {
}
int blockTime = 120;
int blocksPerDay = 86400 / blockTime;
int blocksPerDay = 720;
int blockCalcClearance = blocksPerDay * 5;
QList<int> values = this->data.keys();
if (date <= values.at(0))
return this->data[values.at(0)];
QList<time_t> values = this->data.keys();
// If timestamp is before epoch, return genesis height.
if (date <= values.at(0)) {
return 1;
}
for (int i = 0; i != values.count(); i++) {
if (values[i] > date) {
return i - 1 < 0 ? this->data[values[i]] : this->data[values[i-1]] - blockCalcClearance;
if (i == 0) {
return 1;
}
return this->data[values[i-1]] - blockCalcClearance;
}
}
// lookup failed, calculate blockheight from last known checkpoint
int lastBlockHeightTime = values.at(values.count() - 1);
time_t lastBlockHeightTime = values.last();
int lastBlockHeight = this->data[lastBlockHeightTime];
int deltaTime = date - lastBlockHeightTime;
time_t deltaTime = date - lastBlockHeightTime;
int deltaBlocks = deltaTime / blockTime;
int blockHeight = (lastBlockHeight + deltaBlocks) - blockCalcClearance;
qDebug() << "Calculated blockheight: " << blockHeight << " from epoch " << date;
return blockHeight;
}
int heightToTimestamp(int height) {
time_t heightToTimestamp(int height) {
// @TODO: most likely inefficient, refactor
QMap<int, int>::iterator i;
int timestamp = 0;
QMap<time_t, int>::iterator i;
time_t timestamp = 0;
int heightData = 1;
for (i = this->data.begin(); i != this->data.end(); ++i) {
int ts = i.key();
time_t ts = i.key();
if (i.value() > height)
return timestamp;
timestamp = ts;
@ -78,11 +87,13 @@ struct RestoreHeightLookup {
// initialize this class using a lookup table, e.g `:/assets/restore_heights_monero_mainnet.txt`/
auto rtn = new RestoreHeightLookup(type);
auto data= Utils::barrayToString(Utils::fileOpen(fn));
QMap<int, int> _data;
for (const auto &line: data.split('\n')) {
if(line.trimmed().isEmpty()) continue;
if (line.trimmed().isEmpty()) {
continue;
}
auto spl = line.trimmed().split(':');
rtn->data[spl.at(0).toInt()] = spl.at(1).toInt();
rtn->data[(time_t)spl.at(0).toInt()] = spl.at(1).toInt();
}
return rtn;
}

106
src/utils/Seed.cpp Normal file
View file

@ -0,0 +1,106 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2020-2022 The Monero Project
#include "Seed.h"
Seed::Seed(Type type, NetworkType::Type networkType, QString language)
: type(type), networkType(networkType), language(std::move(language))
{
// We only support the creation of Tevador-style seeds for now.
if (this->type != Type::TEVADOR) {
this->errorString = "Unsupported seed type";
return;
}
// We only support the creation of English language seeds for now.
if (this->language != "English") {
this->errorString = "Unsupported seed language";
return;
}
this->time = std::time(nullptr);
try {
monero_seed seed(this->time, constants::coinName);
std::stringstream buffer;
buffer << seed;
this->mnemonic = QString::fromStdString(buffer.str()).split(" ");
buffer.str(std::string());
buffer << seed.key();
this->spendKey = QString::fromStdString(buffer.str());
}
catch (const std::exception &e) {
this->errorString = QString::fromStdString(e.what());
return;
}
// Basic check against seed library implementation issues
if (m_insecureSeeds.contains(this->spendKey)) {
this->errorString = "Insecure spendkey";
return;
}
this->setRestoreHeight();
}
Seed::Seed(Type type, QStringList mnemonic, NetworkType::Type networkType)
: type(type), mnemonic(std::move(mnemonic)), networkType(networkType)
{
if (m_seedLength[this->type] != this->mnemonic.length()) {
this->errorString = "Invalid seed length";
return;
}
if (this->type == Type::POLYSEED) {
this->errorString = "Unsupported seed type";
return;
}
if (this->type == Type::TEVADOR) {
try {
monero_seed seed(this->mnemonic.join(" ").toStdString(), constants::coinName);
this->time = seed.date();
this->setRestoreHeight();
std::stringstream buffer;
buffer << seed.key();
this->spendKey = QString::fromStdString(buffer.str());
// Tevador style seeds have built-in error correction
// Any word can be replaced with 'xxxx' to recover the original word
this->correction = QString::fromStdString(seed.correction());
if (!this->correction.isEmpty()) {
buffer.str(std::string());
buffer << seed;
int index = this->mnemonic.indexOf("xxxx");
this->mnemonic.replace(index, this->correction);
}
}
catch (const std::exception &e) {
this->errorString = e.what();
return;
}
}
}
void Seed::setRestoreHeight(int height) {
auto now = std::time(nullptr);
auto nowClearance = 3600 * 24;
auto currentBlockHeight = appData()->restoreHeights[this->networkType]->dateToHeight(now - nowClearance);
if (height >= currentBlockHeight + nowClearance) {
qWarning() << "unrealistic restore height detected, setting to current blockheight instead: " << currentBlockHeight;
this->restoreHeight = currentBlockHeight;
} else {
this->restoreHeight = height;
}
}
void Seed::setRestoreHeight() {
// Ignore the embedded restore date, new wallets should sync from the current block height.
this->restoreHeight = appData()->restoreHeights[networkType]->dateToHeight(this->time);
}
Seed::Seed() = default;

62
src/utils/Seed.h Normal file
View file

@ -0,0 +1,62 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2020-2022 The Monero Project
#ifndef FEATHER_SEED_H
#define FEATHER_SEED_H
#include "constants.h"
#include "libwalletqt/Wallet.h"
#include "monero_seed/monero_seed.hpp"
#include "utils/AppData.h"
#include <QString>
#include <QStringList>
#include <sstream>
#include <utility>
struct Seed {
enum Type {
MONERO = 0, // 25 word seeds
TEVADOR, // 14 word seeds
POLYSEED, // 16 word seeds
};
NetworkType::Type networkType = NetworkType::MAINNET;
QString coin;
QString language;
Type type = Type::TEVADOR;
QStringList mnemonic;
QString spendKey;
QString correction;
time_t time{};
int restoreHeight = 0;
QString errorString;
explicit Seed();
explicit Seed(Type type, NetworkType::Type networkType = NetworkType::MAINNET, QString language = "English");
explicit Seed(Type type, QStringList mnemonic, NetworkType::Type networkType = NetworkType::MAINNET);
void setRestoreHeight(int height);
private:
void setRestoreHeight();
QStringList m_insecureSeeds = {
"0000000000000000000000000000000000000000000000000000000000000000",
"1111111111111111111111111111111111111111111111111111111111111111",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
};
QMap<Seed::Type, int> m_seedLength {
{Type::MONERO, 25},
{Type::TEVADOR, 14},
{Type::POLYSEED, 16}
};
};
#endif //FEATHER_SEED_H

View file

@ -9,7 +9,7 @@
#include <QMessageBox>
#include <monero_seed/wordlist.hpp> // tevador 14 word
#include "utils/FeatherSeed.h"
#include "utils/Seed.h"
#include "constants.h"
#include <mnemonics/electrum-words.h>
@ -60,13 +60,13 @@ PageWalletRestoreSeed::PageWalletRestoreSeed(WizardFields *fields, QWidget *pare
void PageWalletRestoreSeed::onSeedTypeToggled() {
if (ui->radio14->isChecked()) {
m_mode = &m_tevador;
m_fields->seedType = SeedType::TEVADOR;
m_fields->seedType = Seed::Type::TEVADOR;
ui->seedEdit->setPlaceholderText("Enter 14 word seed..");
ui->group_seedLanguage->hide();
}
else if (ui->radio25->isChecked()) {
m_mode = &m_legacy;
m_fields->seedType = SeedType::MONERO;
m_fields->seedType = Seed::Type::MONERO;
ui->seedEdit->setPlaceholderText("Enter 25 word seed..");
ui->group_seedLanguage->show();
}
@ -128,7 +128,8 @@ bool PageWalletRestoreSeed::validatePage() {
}
}
auto _seed = FeatherSeed(constants::networkType, QString::fromStdString(constants::coinName), constants::seedLanguage, seedSplit);
Seed _seed = Seed(m_mode->length == 14 ? Seed::Type::TEVADOR : Seed::Type::MONERO, seedSplit, constants::networkType);
if (!_seed.errorString.isEmpty()) {
QMessageBox::warning(this, "Invalid seed", QString("Invalid seed:\n\n%1").arg(_seed.errorString));
ui->seedEdit->setStyleSheet(errStyle);

View file

@ -5,6 +5,7 @@
#include "PageWalletSeed.h"
#include "ui_PageWalletSeed.h"
#include "constants.h"
#include "Seed.h"
#include <QMessageBox>
@ -15,6 +16,10 @@ PageWalletSeed::PageWalletSeed(WizardFields *fields, QWidget *parent)
{
ui->setupUi(this);
ui->frame_invalidSeed->hide();
QPixmap warningIcon = QPixmap(":/assets/images/warning.png");
ui->warningIcon->setPixmap(warningIcon.scaledToWidth(32, Qt::SmoothTransformation));
QPixmap pixmap = QPixmap(":/assets/images/seed.png");
ui->seedIcon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation));
@ -48,13 +53,21 @@ void PageWalletSeed::seedRoulette(int count) {
}
void PageWalletSeed::generateSeed() {
Seed seed;
do {
FeatherSeed seed = FeatherSeed(constants::networkType, QString::fromStdString(constants::coinName), constants::seedLanguage);
seed = Seed(Seed::Type::TEVADOR);
m_mnemonic = seed.mnemonic.join(" ");
m_restoreHeight = seed.restoreHeight;
} while (m_mnemonic.split(" ").length() != 14); // https://github.com/tevador/monero-seed/issues/2
this->displaySeed(m_mnemonic);
if (!seed.errorString.isEmpty()) {
ui->frame_invalidSeed->show();
ui->frame_seedDisplay->hide();
m_seedError = true;
}
}
void PageWalletSeed::displaySeed(const QString &seed){
@ -102,3 +115,7 @@ bool PageWalletSeed::validatePage() {
return true;
}
bool PageWalletSeed::isComplete() const {
return !m_seedError;
}

View file

@ -22,6 +22,7 @@ public:
explicit PageWalletSeed(WizardFields *fields, QWidget *parent = nullptr);
void initializePage() override;
bool validatePage() override;
bool isComplete() const override;
int nextId() const override;
public slots:
@ -42,6 +43,7 @@ private:
QString m_mnemonic;
int m_restoreHeight;
bool m_seedError = false;
bool m_roulette = false;
int m_rouletteSpin = 15;
};

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>877</width>
<height>408</height>
<height>625</height>
</rect>
</property>
<property name="windowTitle">
@ -79,6 +79,72 @@
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_invalidSeed">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_17">
<item>
<widget class="QLabel" name="warningIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>icon</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Feather was unable to generate a valid seed.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>This should never happen.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>Please contact the developers immediately.</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -95,6 +161,27 @@
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="frame_seedDisplay">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_22">
<item>
@ -650,6 +737,9 @@
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">

View file

@ -117,15 +117,17 @@ void WalletWizard::onCreateWallet() {
return;
}
auto seed = FeatherSeed(constants::networkType, QString::fromStdString(constants::coinName), constants::seedLanguage, m_wizardFields.seed.split(" "));
auto seed = Seed(m_wizardFields.seedType, m_wizardFields.seed.split(" "), constants::networkType);
// If we're connected to the websocket, use the reported height for new wallets to skip initial synchronization.
if (m_wizardFields.mode == WizardMode::CreateWallet && currentBlockHeight > 0) {
qInfo() << "New wallet, setting restore height to latest blockheight: " << currentBlockHeight;
seed.setRestoreHeight(currentBlockHeight);
seed.restoreHeight = currentBlockHeight;
}
if (m_wizardFields.mode == WizardMode::RestoreFromSeed && m_wizardFields.seedType == SeedType::MONERO)
if (m_wizardFields.mode == WizardMode::RestoreFromSeed && m_wizardFields.seedType == Seed::Type::MONERO) {
seed.setRestoreHeight(m_wizardFields.restoreHeight);
}
emit createWallet(seed, walletPath, m_wizardFields.password, m_wizardFields.seedLanguage, m_wizardFields.seedOffsetPassphrase);
}

View file

@ -41,7 +41,7 @@ struct WizardFields {
QString secretSpendKey;
WizardMode mode;
int restoreHeight = 0;
SeedType seedType;
Seed::Type seedType;
DeviceType deviceType;
};
@ -74,7 +74,7 @@ signals:
void createWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight);
void createWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, bool deterministic = false);
void createWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset = "");
void createWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset = "");
private slots:
void onCreateWallet();