mirror of https://github.com/oxen-io/oxen-core.git
2876 lines
114 KiB
C++
2876 lines
114 KiB
C++
#include "oxen_name_system.h"
|
|
|
|
#include <oxenc/base32z.h>
|
|
#include <oxenc/base64.h>
|
|
#include <oxenc/hex.h>
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <bitset>
|
|
#include <iterator>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
#include "common/fs-format.h"
|
|
#include "common/hex.h"
|
|
#include "common/oxen.h"
|
|
#include "common/string_util.h"
|
|
#include "crypto/hash.h"
|
|
#include "cryptonote_basic/cryptonote_basic.h"
|
|
#include "cryptonote_basic/cryptonote_basic_impl.h"
|
|
#include "cryptonote_basic/cryptonote_format_utils.h"
|
|
#include "cryptonote_basic/tx_extra.h"
|
|
#include "cryptonote_config.h"
|
|
#include "cryptonote_core/blockchain.h"
|
|
#include "oxen_economy.h"
|
|
|
|
extern "C" {
|
|
#include <sodium/crypto_aead_xchacha20poly1305.h>
|
|
#include <sodium/crypto_generichash.h>
|
|
#include <sodium/crypto_generichash_blake2b.h>
|
|
#include <sodium/crypto_pwhash.h>
|
|
#include <sodium/crypto_secretbox.h>
|
|
#include <sodium/crypto_sign.h>
|
|
#include <sodium/randombytes.h>
|
|
}
|
|
|
|
using cryptonote::hf;
|
|
|
|
namespace ons {
|
|
|
|
namespace log = oxen::log;
|
|
static auto logcat = log::Cat("ons");
|
|
|
|
enum struct ons_sql_type {
|
|
save_owner,
|
|
save_setting,
|
|
save_mapping,
|
|
pruning,
|
|
|
|
get_sentinel_start,
|
|
get_mapping,
|
|
get_mappings,
|
|
get_mappings_by_owner,
|
|
get_mappings_by_owners,
|
|
get_mapping_counts,
|
|
get_owner,
|
|
get_setting,
|
|
get_sentinel_end,
|
|
|
|
internal_cmd,
|
|
};
|
|
|
|
enum struct ons_db_setting_column {
|
|
id,
|
|
top_height,
|
|
top_hash,
|
|
version,
|
|
};
|
|
|
|
enum struct owner_record_column {
|
|
id,
|
|
address,
|
|
};
|
|
|
|
enum struct mapping_record_column {
|
|
id,
|
|
type,
|
|
name_hash,
|
|
encrypted_value,
|
|
txid,
|
|
owner_id,
|
|
backup_owner_id,
|
|
update_height,
|
|
expiration_height,
|
|
_count,
|
|
};
|
|
|
|
static constexpr unsigned char OLD_ENCRYPTION_NONCE[crypto_secretbox_NONCEBYTES] = {};
|
|
std::pair<std::basic_string_view<unsigned char>, std::basic_string_view<unsigned char>>
|
|
ons::mapping_value::value_nonce(mapping_type type) const {
|
|
std::pair<std::basic_string_view<unsigned char>, std::basic_string_view<unsigned char>> result;
|
|
auto& [head, tail] = result;
|
|
head = {buffer.data(), len};
|
|
if ((type == mapping_type::session &&
|
|
len != SESSION_PUBLIC_KEY_BINARY_LENGTH + crypto_aead_xchacha20poly1305_ietf_ABYTES +
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) ||
|
|
len < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES /* shouldn't occur, but just in case */)
|
|
tail = {OLD_ENCRYPTION_NONCE, sizeof(OLD_ENCRYPTION_NONCE)};
|
|
else {
|
|
tail = head.substr(len - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
head.remove_suffix(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string ons::mapping_value::to_readable_value(
|
|
cryptonote::network_type nettype, ons::mapping_type type) const {
|
|
std::string result;
|
|
if (is_lokinet_type(type)) {
|
|
result = oxenc::to_base32z(to_view()) + ".loki";
|
|
} else if (type == ons::mapping_type::wallet) {
|
|
std::optional<cryptonote::address_parse_info> addr = get_wallet_address_info();
|
|
if (addr) {
|
|
result = cryptonote::get_account_address_as_str(
|
|
nettype, (*addr).is_subaddress, (*addr).address);
|
|
} else {
|
|
result = oxenc::to_hex(to_view());
|
|
}
|
|
} else {
|
|
result = oxenc::to_hex(to_view());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::string ons_extra_string(
|
|
cryptonote::network_type nettype, cryptonote::tx_extra_oxen_name_system const& data) {
|
|
std::string extra = "ONS Extra={";
|
|
auto append = std::back_inserter(extra);
|
|
if (data.is_buying())
|
|
fmt::format_to(
|
|
append,
|
|
"owner={}, backup_owner={}",
|
|
data.owner.to_string(nettype),
|
|
(data.backup_owner ? data.backup_owner.to_string(nettype) : "(none)"));
|
|
else if (data.is_renewing())
|
|
extra += "renewal";
|
|
else
|
|
fmt::format_to(append, "signature={}", tools::type_to_hex(data.signature.data));
|
|
|
|
fmt::format_to(append, ", type={}, name_hash={}}}", data.type, data.name_hash);
|
|
return extra;
|
|
}
|
|
|
|
/// Clears any existing bindings
|
|
bool clear_bindings(sql_compiled_statement& s) {
|
|
return SQLITE_OK == sqlite3_clear_bindings(s.statement);
|
|
}
|
|
|
|
/// Resets
|
|
bool reset(sql_compiled_statement& s) {
|
|
return SQLITE_OK == sqlite3_reset(s.statement);
|
|
}
|
|
|
|
int step(sql_compiled_statement& s) {
|
|
return sqlite3_step(s.statement);
|
|
}
|
|
|
|
/// `bind()` binds a particular parameter to a statement by index. The bind type is inferred
|
|
/// from the argument.
|
|
|
|
// Small (<=32 bits) integers
|
|
template <typename T, std::enable_if_t<std::is_integral_v<T> && (sizeof(T) <= 4), int> = 0>
|
|
bool bind(sql_compiled_statement& s, int index, const T& val) {
|
|
return SQLITE_OK == sqlite3_bind_int(s.statement, index, val);
|
|
}
|
|
|
|
// Big (>32 bits) integers
|
|
template <typename T, std::enable_if_t<std::is_integral_v<T> && (sizeof(T) > 4), int> = 0>
|
|
bool bind(sql_compiled_statement& s, int index, const T& val) {
|
|
return SQLITE_OK == sqlite3_bind_int64(s.statement, index, val);
|
|
}
|
|
|
|
// Floats/doubles
|
|
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
|
|
bool bind(sql_compiled_statement& s, int index, const T& val) {
|
|
return SQLITE_OK == sqlite3_bind_double(s.statement, index, val);
|
|
}
|
|
|
|
// Binds null
|
|
bool bind(sql_compiled_statement& s, int index, std::nullptr_t) {
|
|
return SQLITE_OK == sqlite3_bind_null(s.statement, index);
|
|
}
|
|
|
|
// Binds a std::optional<T>: binds a T if set, otherwise binds a NULL
|
|
template <typename T>
|
|
bool bind(sql_compiled_statement& s, int index, const std::optional<T>& val) {
|
|
if (val)
|
|
return bind(s, index, *val);
|
|
return bind(s, index, nullptr);
|
|
}
|
|
|
|
// text, from a referenced string (which must be kept alive)
|
|
bool bind(sql_compiled_statement& s, int index, std::string_view text) {
|
|
return SQLITE_OK ==
|
|
sqlite3_bind_text(s.statement, index, text.data(), text.size(), nullptr /*dtor*/);
|
|
}
|
|
|
|
/* Currently unused; comment out until needed to avoid a compiler warning
|
|
// text, from a temporary std::string; ownership of the string data is transferred to sqlite3
|
|
bool bind(sql_compiled_statement& s, int index, std::string&& text)
|
|
{
|
|
// Assume ownership and let sqlite3 destroy when finished
|
|
auto local_text = new std::string{std::move(text)};
|
|
if (SQLITE_OK == sqlite3_bind_text(s.statement, index, local_text->data(), local_text->size(),
|
|
[](void* local) { delete reinterpret_cast<std::string*>(local); }))
|
|
return true;
|
|
delete local_text;
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
// Simple decorator around a string_view so that you can pass a blob into `bind` by wrapping it
|
|
// with a `blob_view` such as:
|
|
//
|
|
// bind(s, 123, blob_view{data, size});
|
|
// auto data = get<blob_view>(s, 2);
|
|
//
|
|
struct blob_view {
|
|
std::string_view data;
|
|
/// Constructor that simply forwards anything to the `data` (string_view) member constructor
|
|
template <typename... T>
|
|
explicit blob_view(T&&... args) : data{std::forward<T>(args)...} {}
|
|
blob_view(const unsigned char* data, size_t size) :
|
|
blob_view{reinterpret_cast<const char*>(data), size} {}
|
|
};
|
|
|
|
// Binds a blob wrapped in a blob_view decorator
|
|
bool bind(sql_compiled_statement& s, int index, blob_view blob) {
|
|
return SQLITE_OK ==
|
|
sqlite3_bind_blob(
|
|
s.statement, index, blob.data.data(), blob.data.size(), nullptr /*dtor*/);
|
|
}
|
|
|
|
// Binds a variant of bindable types; calls one of the above according to the contained type
|
|
template <typename... T>
|
|
bool bind(sql_compiled_statement& s, int index, const std::variant<T...>& v) {
|
|
return var::visit([&](const auto& val) { return ons::bind(s, index, val); }, v);
|
|
}
|
|
|
|
template <typename T>
|
|
constexpr bool is_int_enum_impl() {
|
|
if constexpr (std::is_enum_v<T>)
|
|
return std::is_same_v<std::underlying_type_t<T>, int>;
|
|
else
|
|
return false;
|
|
}
|
|
template <typename T>
|
|
constexpr bool is_int_enum = is_int_enum_impl<T>();
|
|
|
|
// Binds, but gives index as an enum class
|
|
template <typename T, typename I, std::enable_if_t<is_int_enum<I>, int> = 0>
|
|
bool bind(sql_compiled_statement& s, I index, T&& val) {
|
|
return ons::bind(s, static_cast<int>(index), std::forward<T>(val));
|
|
}
|
|
|
|
template <int... I, typename... T>
|
|
bool bind_all_impl(sql_compiled_statement& s, std::integer_sequence<int, I...>, T&&... args) {
|
|
clear_bindings(s);
|
|
for (bool r : {ons::bind(s, I + 1, std::forward<T>(args))...})
|
|
if (!r)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Full statement binding; this lets you do something like:
|
|
//
|
|
// bind_all(st, 1, "hi", 123);
|
|
//
|
|
// which is equivalent to:
|
|
//
|
|
// clear_bindings(st);
|
|
// st.bind(st, 1, 1);
|
|
// st.bind(st, 2, "hi");
|
|
// st.bind(st, 3, 123);
|
|
//
|
|
// (Binding of blobs through this interface is not supported).
|
|
template <typename... T>
|
|
bool bind_all(sql_compiled_statement& s, T&&... args) {
|
|
return bind_all_impl(
|
|
s, std::make_integer_sequence<int, sizeof...(T)>{}, std::forward<T>(args)...);
|
|
}
|
|
|
|
// Full statement binding from a container of bind()-able values; clears existing bindings, then
|
|
// binds the contained values.
|
|
template <typename Container>
|
|
bool bind_container(sql_compiled_statement& s, const Container& c) {
|
|
clear_bindings(s);
|
|
int bind_pos = 1;
|
|
for (const auto& v : c)
|
|
if (!ons::bind(s, bind_pos++, v))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/// Retrieve a type from an executed statement.
|
|
|
|
// Small (<=32 bits) integers
|
|
template <typename T, std::enable_if_t<std::is_integral_v<T> && (sizeof(T) <= 32), int> = 0>
|
|
T get(sql_compiled_statement& s, int index) {
|
|
return static_cast<T>(sqlite3_column_int(s.statement, index));
|
|
}
|
|
|
|
// Big (>32 bits) integers
|
|
template <typename T, std::enable_if_t<std::is_integral_v<T> && (sizeof(T) > 32), int> = 0>
|
|
T get(sql_compiled_statement& s, int index) {
|
|
return static_cast<T>(sqlite3_column_int64(s.statement, index));
|
|
}
|
|
|
|
// Floats/doubles
|
|
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
|
|
T get(sql_compiled_statement& s, int index) {
|
|
return static_cast<T>(sqlite3_column_double(s.statement, index));
|
|
}
|
|
|
|
// text, via a string_view pointing at the text data
|
|
template <typename T, std::enable_if_t<std::is_same_v<T, std::string_view>, int> = 0>
|
|
std::string_view get(sql_compiled_statement& s, int index) {
|
|
return {reinterpret_cast<const char*>(sqlite3_column_text(s.statement, index)),
|
|
static_cast<size_t>(sqlite3_column_bytes(s.statement, index))};
|
|
}
|
|
|
|
// text, copied into a std::string
|
|
template <typename T, std::enable_if_t<std::is_same_v<T, std::string>, int> = 0>
|
|
std::string get(sql_compiled_statement& s, int index) {
|
|
return {reinterpret_cast<const char*>(sqlite3_column_text(s.statement, index)),
|
|
static_cast<size_t>(sqlite3_column_bytes(s.statement, index))};
|
|
}
|
|
|
|
// blob_view pointing at the blob data
|
|
template <typename T, std::enable_if_t<std::is_same_v<T, blob_view>, int> = 0>
|
|
blob_view get(sql_compiled_statement& s, int index) {
|
|
return blob_view{
|
|
reinterpret_cast<const char*>(sqlite3_column_blob(s.statement, index)),
|
|
static_cast<size_t>(sqlite3_column_bytes(s.statement, index))};
|
|
}
|
|
|
|
template <typename T>
|
|
constexpr bool is_optional = false;
|
|
template <typename T>
|
|
constexpr bool is_optional<std::optional<T>> = true;
|
|
|
|
// Gets a potentially null value; returns a std::nullopt if the column contains NULL, otherwise
|
|
// return a value via get<T>(...).
|
|
template <typename T, std::enable_if_t<is_optional<T>, int> = 0>
|
|
T get(sql_compiled_statement& s, int index) {
|
|
if (sqlite3_column_type(s.statement, index) == SQLITE_NULL)
|
|
return std::nullopt;
|
|
return get<typename T::value_type>(s, index);
|
|
}
|
|
|
|
// Forwards to any of the above, but takes an enum class instead of an int
|
|
template <typename T, typename I, std::enable_if_t<is_int_enum<I>, int> = 0>
|
|
T get(sql_compiled_statement& s, I index) {
|
|
return get<T>(s, static_cast<int>(index));
|
|
}
|
|
|
|
// Wrapper around get that assigns to the given reference.
|
|
// get(st, 3, myvar);
|
|
// is equivalent to:
|
|
// myvar = get<decltype(myvar)>(st, 3)
|
|
template <typename T, typename I>
|
|
void get(sql_compiled_statement& s, I index, T& val) {
|
|
val = get<T>(s, index);
|
|
}
|
|
|
|
template <typename I>
|
|
bool sql_copy_blob(sql_compiled_statement& statement, I column, void* dest, size_t dest_size) {
|
|
|
|
auto blob = get<blob_view>(statement, column);
|
|
if (blob.data.size() != dest_size) {
|
|
log::warning(
|
|
logcat,
|
|
"Unexpected blob size={}, in ONS DB does not match expected size={}",
|
|
blob.data.size(),
|
|
dest_size);
|
|
assert(blob.data.size() == dest_size);
|
|
return false;
|
|
}
|
|
|
|
std::memcpy(dest, blob.data.data(), blob.data.size());
|
|
return true;
|
|
}
|
|
|
|
mapping_record sql_get_mapping_from_statement(sql_compiled_statement& statement) {
|
|
mapping_record result = {};
|
|
auto type_int = get<uint16_t>(statement, mapping_record_column::type);
|
|
if (type_int >= tools::enum_count<mapping_type>)
|
|
return result;
|
|
|
|
result.type = static_cast<mapping_type>(type_int);
|
|
get(statement, mapping_record_column::id, result.id);
|
|
get(statement, mapping_record_column::update_height, result.update_height);
|
|
get(statement, mapping_record_column::expiration_height, result.expiration_height);
|
|
get(statement, mapping_record_column::owner_id, result.owner_id);
|
|
get(statement, mapping_record_column::backup_owner_id, result.backup_owner_id);
|
|
|
|
// Copy encrypted_value
|
|
{
|
|
auto value = get<std::string_view>(statement, mapping_record_column::encrypted_value);
|
|
if (value.size() > result.encrypted_value.buffer.size()) {
|
|
log::error(
|
|
logcat,
|
|
"Unexpected encrypted value blob with size={}, in ONS db larger than the "
|
|
"available size={}",
|
|
value.size(),
|
|
result.encrypted_value.buffer.size());
|
|
return result;
|
|
}
|
|
result.encrypted_value.len = value.size();
|
|
result.encrypted_value.encrypted = true;
|
|
std::memcpy(&result.encrypted_value.buffer[0], value.data(), value.size());
|
|
}
|
|
|
|
// Copy name hash
|
|
{
|
|
auto value = get<std::string_view>(statement, mapping_record_column::name_hash);
|
|
result.name_hash.append(value.data(), value.size());
|
|
}
|
|
|
|
if (!sql_copy_blob(
|
|
statement, mapping_record_column::txid, result.txid.data(), result.txid.size()))
|
|
return result;
|
|
|
|
int owner_column = tools::enum_count<mapping_record_column>;
|
|
if (!sql_copy_blob(statement, owner_column, &result.owner, sizeof(result.owner)))
|
|
return result;
|
|
|
|
if (result.backup_owner_id > 0) {
|
|
if (!sql_copy_blob(
|
|
statement,
|
|
owner_column + 1,
|
|
&result.backup_owner,
|
|
sizeof(result.backup_owner)))
|
|
return result;
|
|
}
|
|
|
|
result.loaded = true;
|
|
return result;
|
|
}
|
|
|
|
bool sql_run_statement(ons_sql_type type, sql_compiled_statement& statement, void* context) {
|
|
assert(statement);
|
|
bool data_loaded = false;
|
|
bool result = false;
|
|
|
|
for (bool infinite_loop = true; infinite_loop;) {
|
|
int step_result = step(statement);
|
|
switch (step_result) {
|
|
case SQLITE_ROW: {
|
|
switch (type) {
|
|
default:
|
|
log::error(
|
|
logcat,
|
|
"Unhandled ons type enum with value: {}, in: {}",
|
|
(int)type,
|
|
__func__);
|
|
break;
|
|
|
|
case ons_sql_type::internal_cmd: break;
|
|
case ons_sql_type::get_owner: {
|
|
auto* entry = reinterpret_cast<owner_record*>(context);
|
|
get(statement, owner_record_column::id, entry->id);
|
|
if (!sql_copy_blob(
|
|
statement,
|
|
owner_record_column::address,
|
|
&entry->address,
|
|
sizeof(entry->address)))
|
|
return false;
|
|
data_loaded = true;
|
|
} break;
|
|
|
|
case ons_sql_type::get_setting: {
|
|
auto* entry = reinterpret_cast<settings_record*>(context);
|
|
get(statement, ons_db_setting_column::top_height, entry->top_height);
|
|
if (!sql_copy_blob(
|
|
statement,
|
|
ons_db_setting_column::top_hash,
|
|
entry->top_hash.data(),
|
|
entry->top_hash.size()))
|
|
return false;
|
|
get(statement, ons_db_setting_column::version, entry->version);
|
|
data_loaded = true;
|
|
} break;
|
|
|
|
case ons_sql_type::get_mappings_by_owners: [[fallthrough]];
|
|
case ons_sql_type::get_mappings_by_owner: [[fallthrough]];
|
|
case ons_sql_type::get_mappings: [[fallthrough]];
|
|
case ons_sql_type::get_mapping: {
|
|
if (mapping_record tmp_entry =
|
|
sql_get_mapping_from_statement(statement)) {
|
|
data_loaded = true;
|
|
if (type == ons_sql_type::get_mapping)
|
|
*static_cast<mapping_record*>(context) = std::move(tmp_entry);
|
|
else
|
|
static_cast<std::vector<mapping_record>*>(context)->push_back(
|
|
std::move(tmp_entry));
|
|
}
|
|
} break;
|
|
|
|
case ons_sql_type::get_mapping_counts: {
|
|
auto& counts = *static_cast<std::map<mapping_type, int>*>(context);
|
|
std::underlying_type_t<mapping_type> type_val;
|
|
int count;
|
|
get(statement, 0, type_val);
|
|
get(statement, 1, count);
|
|
counts.emplace(static_cast<mapping_type>(type_val), count);
|
|
data_loaded = true;
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case SQLITE_BUSY: break;
|
|
case SQLITE_DONE: {
|
|
infinite_loop = false;
|
|
result = (type > ons_sql_type::get_sentinel_start &&
|
|
type < ons_sql_type::get_sentinel_end)
|
|
? data_loaded
|
|
: true;
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
log::info(
|
|
logcat,
|
|
"Failed to execute statement: {}, reason: {}",
|
|
sqlite3_sql(statement.statement),
|
|
sqlite3_errstr(step_result));
|
|
infinite_loop = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
reset(statement);
|
|
clear_bindings(statement);
|
|
return result;
|
|
}
|
|
|
|
/// Does a clear_bindings, bind_all, and then sql_run_statement. First three arguments go to
|
|
/// sql_run_statement, the rest go to bind_all(statement, ...) (which does the clear_bindings).
|
|
template <typename... T>
|
|
bool bind_and_run(
|
|
ons_sql_type type, sql_compiled_statement& statement, void* context, T&&... bind_args) {
|
|
bind_all(statement, std::forward<T>(bind_args)...);
|
|
return sql_run_statement(type, statement, context);
|
|
}
|
|
|
|
} // end anonymous namespace
|
|
|
|
using namespace std::literals;
|
|
|
|
using stringtypemap = std::pair<std::string_view, mapping_type>;
|
|
static constexpr std::array ons_str_type_mappings = {
|
|
stringtypemap{"5"sv, mapping_type::lokinet_10years},
|
|
stringtypemap{"4"sv, mapping_type::lokinet_5years},
|
|
stringtypemap{"3"sv, mapping_type::lokinet_2years},
|
|
stringtypemap{"2"sv, mapping_type::lokinet},
|
|
stringtypemap{"1"sv, mapping_type::wallet},
|
|
stringtypemap{"0"sv, mapping_type::session},
|
|
stringtypemap{"session"sv, mapping_type::session},
|
|
stringtypemap{"wallet"sv, mapping_type::wallet},
|
|
stringtypemap{"lokinet"sv, mapping_type::lokinet},
|
|
stringtypemap{"lokinet_2years"sv, mapping_type::lokinet_2years},
|
|
stringtypemap{"lokinet_5years"sv, mapping_type::lokinet_5years},
|
|
stringtypemap{"lokinet_10years"sv, mapping_type::lokinet_10years}};
|
|
|
|
std::optional<mapping_type> parse_ons_type(std::string input) {
|
|
// Lower-case the input:
|
|
for (auto& c : input)
|
|
if (c >= 'A' && c <= 'Z')
|
|
c += ('A' - 'a');
|
|
|
|
for (const auto& [str, map] : ons_str_type_mappings)
|
|
if (str == input)
|
|
return map;
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
using inttypemap = std::pair<uint16_t, mapping_type>;
|
|
static constexpr std::array ons_int_type_mappings = {
|
|
inttypemap{5, mapping_type::lokinet_10years},
|
|
inttypemap{4, mapping_type::lokinet_5years},
|
|
inttypemap{3, mapping_type::lokinet_2years},
|
|
inttypemap{2, mapping_type::lokinet},
|
|
inttypemap{1, mapping_type::wallet},
|
|
inttypemap{0, mapping_type::session}};
|
|
std::optional<mapping_type> parse_ons_type(uint16_t input) {
|
|
for (const auto& [inttype, map] : ons_int_type_mappings)
|
|
if (inttype == input)
|
|
return map;
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool mapping_record::active(uint64_t blockchain_height) const {
|
|
if (!loaded)
|
|
return false;
|
|
return !expiration_height || blockchain_height <= *expiration_height;
|
|
}
|
|
|
|
bool sql_compiled_statement::compile(std::string_view query, bool optimise_for_multiple_usage) {
|
|
sqlite3_stmt* st;
|
|
#if SQLITE_VERSION_NUMBER >= 3020000
|
|
int prepare_result = sqlite3_prepare_v3(
|
|
nsdb.db,
|
|
query.data(),
|
|
query.size(),
|
|
optimise_for_multiple_usage ? SQLITE_PREPARE_PERSISTENT : 0,
|
|
&st,
|
|
nullptr /*pzTail*/);
|
|
#else
|
|
int prepare_result =
|
|
sqlite3_prepare_v2(nsdb.db, query.data(), query.size(), &st, nullptr /*pzTail*/);
|
|
#endif
|
|
|
|
if (prepare_result != SQLITE_OK) {
|
|
log::error(
|
|
logcat,
|
|
"Can not compile SQL statement:\n{}\nReason: {}",
|
|
query,
|
|
sqlite3_errstr(prepare_result));
|
|
return false;
|
|
}
|
|
sqlite3_finalize(statement);
|
|
statement = st;
|
|
return true;
|
|
}
|
|
|
|
sql_compiled_statement& sql_compiled_statement::operator=(sql_compiled_statement&& from) {
|
|
sqlite3_finalize(statement);
|
|
statement = from.statement;
|
|
from.statement = nullptr;
|
|
return *this;
|
|
}
|
|
|
|
sql_compiled_statement::~sql_compiled_statement() {
|
|
sqlite3_finalize(statement);
|
|
}
|
|
|
|
sqlite3* init_oxen_name_system(const fs::path& file_path, bool read_only) {
|
|
sqlite3* result = nullptr;
|
|
int sql_init = sqlite3_initialize();
|
|
if (sql_init != SQLITE_OK) {
|
|
log::error(logcat, "Failed to initialize sqlite3: {}", sqlite3_errstr(sql_init));
|
|
return nullptr;
|
|
}
|
|
|
|
int const flags = read_only ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE;
|
|
int sql_open = sqlite3_open_v2(file_path.u8string().c_str(), &result, flags, nullptr);
|
|
if (sql_open != SQLITE_OK) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to open ONS db at: {}, reason: {}",
|
|
file_path,
|
|
sqlite3_errstr(sql_open));
|
|
return nullptr;
|
|
}
|
|
|
|
/*
|
|
(DB) Changes are appended into a separate WAL (Write Ahead Logging) file.
|
|
A COMMIT occurs when a special record indicating a commit is appended to
|
|
the WAL. Thus a COMMIT can happen without ever writing to the original
|
|
database, which allows readers to continue operating from the original
|
|
unaltered database while changes are simultaneously being committed into the
|
|
WAL. Multiple transactions can be appended to the end of a single WAL file.
|
|
*/
|
|
int exec = sqlite3_exec(result, "PRAGMA journal_mode = WAL", nullptr, nullptr, nullptr);
|
|
if (exec != SQLITE_OK) {
|
|
log::error(logcat, "Failed to set journal mode to WAL: {}", sqlite3_errstr(exec));
|
|
return nullptr;
|
|
}
|
|
|
|
/*
|
|
In WAL mode when synchronous is NORMAL (1), the WAL file is synchronized
|
|
before each checkpoint and the database file is synchronized after each
|
|
completed checkpoint and the WAL file header is synchronized when a WAL file
|
|
begins to be reused after a checkpoint, but no sync operations occur during
|
|
most transactions.
|
|
*/
|
|
exec = sqlite3_exec(result, "PRAGMA synchronous = NORMAL", nullptr, nullptr, nullptr);
|
|
if (exec != SQLITE_OK) {
|
|
log::error(logcat, "Failed to set synchronous mode to NORMAL: {}", sqlite3_errstr(exec));
|
|
return nullptr;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<mapping_type> all_mapping_types(hf hf_version) {
|
|
std::vector<mapping_type> result;
|
|
result.reserve(2);
|
|
if (hf_version >= hf::hf15_ons)
|
|
result.push_back(mapping_type::session);
|
|
if (hf_version >= hf::hf16_pulse)
|
|
result.push_back(mapping_type::lokinet);
|
|
if (hf_version >= hf::hf18)
|
|
result.push_back(mapping_type::wallet);
|
|
return result;
|
|
}
|
|
|
|
std::optional<uint64_t> expiry_blocks(cryptonote::network_type nettype, mapping_type type) {
|
|
std::optional<uint64_t> result;
|
|
if (is_lokinet_type(type)) {
|
|
// For testnet we shorten 1-, 2-, and 5-year renewals to 1/2/5 days with 1-day renewal, but
|
|
// leave 10 years alone to allow long-term registrations on testnet.
|
|
const bool testnet_short = nettype == cryptonote::network_type::TESTNET &&
|
|
type != mapping_type::lokinet_10years;
|
|
|
|
result = cryptonote::BLOCKS_PER_DAY * REGISTRATION_YEAR_DAYS *
|
|
(type == mapping_type::lokinet ? 1
|
|
: type == mapping_type::lokinet_2years ? 2
|
|
: type == mapping_type::lokinet_5years ? 5
|
|
: type == mapping_type::lokinet_10years ? 10
|
|
: 0);
|
|
assert(result && *result);
|
|
|
|
if (testnet_short)
|
|
*result /= REGISTRATION_YEAR_DAYS;
|
|
else if (nettype == cryptonote::network_type::FAKECHAIN) // For fakenet testing we shorten
|
|
// 1/2/5/10 years to 2/4/10/20
|
|
// blocks
|
|
*result /= (cryptonote::BLOCKS_PER_DAY * REGISTRATION_YEAR_DAYS / 2);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void append_owner(std::string& buffer, const ons::generic_owner* owner) {
|
|
if (owner) {
|
|
buffer += static_cast<char>(owner->type);
|
|
buffer += owner->type == ons::generic_owner_sig_type::ed25519
|
|
? tools::view_guts(owner->ed25519)
|
|
: tools::view_guts(owner->wallet.address);
|
|
}
|
|
}
|
|
|
|
std::string tx_extra_signature(
|
|
std::string_view value,
|
|
ons::generic_owner const* owner,
|
|
ons::generic_owner const* backup_owner,
|
|
crypto::hash const& prev_txid) {
|
|
static_assert(
|
|
sizeof(crypto::hash) == crypto_generichash_BYTES,
|
|
"Using libsodium generichash for signature hash, require we fit into crypto::hash");
|
|
if (value.size() > mapping_value::BUFFER_SIZE) {
|
|
log::error(
|
|
logcat,
|
|
"Unexpected value len={} greater than the expected capacity={}",
|
|
value.size(),
|
|
mapping_value::BUFFER_SIZE);
|
|
return ""s;
|
|
}
|
|
|
|
std::string result;
|
|
result.reserve(
|
|
mapping_value::BUFFER_SIZE + sizeof(*owner) + sizeof(*backup_owner) +
|
|
sizeof(prev_txid));
|
|
result += value;
|
|
append_owner(result, owner);
|
|
append_owner(result, backup_owner);
|
|
result += tools::view_guts(prev_txid);
|
|
|
|
return result;
|
|
}
|
|
|
|
ons::generic_signature make_ed25519_signature(
|
|
crypto::hash const& hash, crypto::ed25519_secret_key const& skey) {
|
|
ons::generic_signature result = {};
|
|
result.type = ons::generic_owner_sig_type::ed25519;
|
|
crypto_sign_detached(result.ed25519.data(), NULL, hash.data(), hash.size(), skey.data());
|
|
return result;
|
|
}
|
|
|
|
ons::generic_owner make_monero_owner(
|
|
cryptonote::account_public_address const& owner, bool is_subaddress) {
|
|
ons::generic_owner result = {};
|
|
result.type = ons::generic_owner_sig_type::monero;
|
|
result.wallet.address = owner;
|
|
result.wallet.is_subaddress = is_subaddress;
|
|
return result;
|
|
}
|
|
|
|
ons::generic_owner make_ed25519_owner(crypto::ed25519_public_key const& pkey) {
|
|
ons::generic_owner result = {};
|
|
result.type = ons::generic_owner_sig_type::ed25519;
|
|
result.ed25519 = pkey;
|
|
return result;
|
|
}
|
|
|
|
bool parse_owner_to_generic_owner(
|
|
cryptonote::network_type nettype,
|
|
std::string_view owner,
|
|
generic_owner& result,
|
|
std::string* reason) {
|
|
cryptonote::address_parse_info parsed_addr;
|
|
crypto::ed25519_public_key ed_owner;
|
|
if (cryptonote::get_account_address_from_str(parsed_addr, nettype, owner)) {
|
|
result = ons::make_monero_owner(parsed_addr.address, parsed_addr.is_subaddress);
|
|
} else if (owner.size() == 2 * ed_owner.size() && oxenc::is_hex(owner)) {
|
|
oxenc::from_hex(owner.begin(), owner.end(), ed_owner.data());
|
|
result = ons::make_ed25519_owner(ed_owner);
|
|
} else {
|
|
if (reason) {
|
|
char const* type_heuristic = (owner.size() == sizeof(crypto::ed25519_public_key) * 2)
|
|
? "ED25519 Key"
|
|
: "Wallet address";
|
|
*reason = type_heuristic;
|
|
*reason += " provided could not be parsed owner=";
|
|
*reason += owner;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns true if the character is numeric, *lower-case* a-z, or any of the template char values.
|
|
template <char... Extra>
|
|
static constexpr bool char_is_alphanum_or(char c) {
|
|
bool result = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (... || (c == Extra));
|
|
return result;
|
|
}
|
|
// Same as above with no extra char values.
|
|
static constexpr bool char_is_alphanum(char c) {
|
|
return char_is_alphanum_or<>(c);
|
|
}
|
|
|
|
template <typename... T>
|
|
static bool check_condition(
|
|
bool condition, std::string* reason, std::string_view format, T&&... args) {
|
|
if (condition && reason)
|
|
*reason = fmt::format(format, std::forward<T>(args)...);
|
|
return condition;
|
|
}
|
|
|
|
bool validate_ons_name(mapping_type type, std::string name, std::string* reason) {
|
|
bool const is_lokinet = is_lokinet_type(type);
|
|
size_t max_name_len = 0;
|
|
|
|
if (is_lokinet)
|
|
max_name_len = name.find('-') != std::string::npos ? LOKINET_DOMAIN_NAME_MAX
|
|
: LOKINET_DOMAIN_NAME_MAX_NOHYPHEN;
|
|
else if (type == mapping_type::session)
|
|
max_name_len = ons::SESSION_DISPLAY_NAME_MAX;
|
|
else if (type == mapping_type::wallet)
|
|
max_name_len = ons::WALLET_NAME_MAX;
|
|
else {
|
|
if (reason)
|
|
*reason =
|
|
"ONS type={} specifies unhandled mapping type in name validation"_format(type);
|
|
return false;
|
|
}
|
|
|
|
// NOTE: Validate name length
|
|
name = tools::lowercase_ascii_string(name);
|
|
if (check_condition(
|
|
(name.empty() || name.size() > max_name_len),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name's length={} is 0 or "
|
|
"exceeds the maximum length={}, given name={}",
|
|
type,
|
|
name.size(),
|
|
max_name_len,
|
|
name))
|
|
return false;
|
|
|
|
std::string_view name_view{name}; // Will chop this down as we validate each part
|
|
|
|
// NOTE: Validate domain specific requirements
|
|
if (is_lokinet) {
|
|
// LOKINET
|
|
// Domain has to start with an alphanumeric, and can have (alphanumeric or hyphens) in
|
|
// between, the character before the suffix <char>'.loki' must be alphanumeric followed by
|
|
// the suffix '.loki' It's *approximately* this regex, but there are some extra restrictions
|
|
// below
|
|
// ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.loki$
|
|
|
|
// Reserved names:
|
|
// - localhost.loki has special meaning within lokinet (it is always a CNAME to the local
|
|
// address)
|
|
// - loki.loki and snode.loki are prohibited in case someone added .loki or .snode as search
|
|
// domains (in which case the user looking up "foo.loki" would try end up trying to
|
|
// resolve "foo.loki.loki").
|
|
for (auto& reserved : {"localhost.loki"sv, "loki.loki"sv, "snode.loki"sv})
|
|
if (check_condition(
|
|
name == reserved,
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value using protocol reserved "
|
|
"name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
|
|
auto constexpr SHORTEST_DOMAIN = "a.loki"sv;
|
|
if (check_condition(
|
|
name.size() < SHORTEST_DOMAIN.size(),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name is shorter than "
|
|
"the shortest possible name={}, given name={}",
|
|
type,
|
|
SHORTEST_DOMAIN,
|
|
name))
|
|
return false;
|
|
|
|
// Must end with .loki
|
|
auto constexpr SUFFIX = ".loki"sv;
|
|
if (check_condition(
|
|
!tools::ends_with(name_view, SUFFIX),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name does not end "
|
|
"with the domain .loki, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
|
|
name_view.remove_suffix(SUFFIX.size());
|
|
|
|
// All domains containing '--' as 3rd/4th letter are reserved except for xn-- punycode
|
|
// domains
|
|
if (check_condition(
|
|
name_view.size() >= 4 && name_view.substr(2, 2) == "--"sv &&
|
|
!tools::starts_with(name_view, "xn--"sv),
|
|
reason,
|
|
"ONS type={} specifies reserved name `?\?--*.loki': {}",
|
|
type,
|
|
name))
|
|
return false;
|
|
|
|
// Must start with alphanumeric
|
|
if (check_condition(
|
|
!char_is_alphanum(name_view.front()),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name does not start "
|
|
"with an alphanumeric character, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
|
|
name_view.remove_prefix(1);
|
|
|
|
if (!name_view.empty()) {
|
|
// Character preceding .loki must be alphanumeric
|
|
if (check_condition(
|
|
!char_is_alphanum(name_view.back()),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the character "
|
|
"preceding the .loki is not alphanumeric, char={}, name={}",
|
|
type,
|
|
name_view.back(),
|
|
name))
|
|
return false;
|
|
name_view.remove_suffix(1);
|
|
}
|
|
|
|
// Inbetween start and preceding suffix, (alphanumeric or hyphen) characters permitted
|
|
if (check_condition(
|
|
!std::all_of(name_view.begin(), name_view.end(), char_is_alphanum_or<'-'>),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the domain name contains "
|
|
"more than the permitted alphanumeric or hyphen characters, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
} else if (type == mapping_type::session || type == mapping_type::wallet) {
|
|
// SESSION & WALLET
|
|
// Name has to start with a (alphanumeric or underscore), and can have (alphanumeric,
|
|
// hyphens or underscores) in between and must end with a (alphanumeric or underscore)
|
|
// ^[a-z0-9_]([a-z0-9-_]*[a-z0-9_])?$
|
|
|
|
// Must start with (alphanumeric or underscore)
|
|
if (check_condition(
|
|
!char_is_alphanum_or<'_'>(name_view.front()),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name does not start "
|
|
"with an alphanumeric or underscore character, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
name_view.remove_prefix(1);
|
|
|
|
if (!name_view.empty()) {
|
|
// Must NOT end with a hyphen '-'
|
|
if (check_condition(
|
|
!char_is_alphanum_or<'_'>(name_view.back()),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the last character "
|
|
"is a hyphen '-' which is disallowed, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
name_view.remove_suffix(1);
|
|
}
|
|
|
|
// Inbetween start and preceding suffix, (alphanumeric, hyphen or underscore) characters
|
|
// permitted
|
|
if (check_condition(
|
|
!std::all_of(name_view.begin(), name_view.end(), char_is_alphanum_or<'-', '_'>),
|
|
reason,
|
|
"ONS type={} specifies mapping from name->value where the name contains more "
|
|
"than the permitted alphanumeric, underscore or hyphen characters, name={}",
|
|
type,
|
|
name))
|
|
return false;
|
|
} else {
|
|
log::error(logcat, "Type not implemented");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::optional<cryptonote::address_parse_info> encrypted_wallet_value_to_info(
|
|
std::string name, std::string encrypted_value, std::string nonce) {
|
|
std::string lower_name = tools::lowercase_ascii_string(std::move(name));
|
|
mapping_value record(oxenc::from_hex(encrypted_value), oxenc::from_hex(nonce));
|
|
record.decrypt(lower_name, mapping_type::wallet);
|
|
return record.get_wallet_address_info();
|
|
}
|
|
|
|
static bool check_lengths(
|
|
mapping_type type,
|
|
std::string_view value,
|
|
size_t max,
|
|
bool binary_val,
|
|
std::string* reason) {
|
|
bool result;
|
|
if (type == mapping_type::wallet) {
|
|
result =
|
|
(value.size() == (WALLET_ACCOUNT_BINARY_LENGTH_INC_PAYMENT_ID + max) ||
|
|
value.size() == (WALLET_ACCOUNT_BINARY_LENGTH_NO_PAYMENT_ID + max));
|
|
} else {
|
|
result = (value.size() == max);
|
|
}
|
|
if (!result) {
|
|
if (reason) {
|
|
*reason =
|
|
"ONS type={} specifies mapping from name_hash->encrypted_value where the value's length={} does not equal the required length={}, given value={}"_format(
|
|
type, value.size(), max, binary_val ? oxenc::to_hex(value) : value);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// This function checks that the value is valid but it also will copy the value into the
|
|
// mapping_value buffer ready for mapping_value::encrypt()
|
|
bool mapping_value::validate(
|
|
cryptonote::network_type nettype,
|
|
mapping_type type,
|
|
std::string_view value,
|
|
mapping_value* blob,
|
|
std::string* reason) {
|
|
if (blob)
|
|
*blob = {};
|
|
|
|
// Check length of the value
|
|
cryptonote::address_parse_info addr_info = {};
|
|
if (type == mapping_type::wallet) {
|
|
if (value.empty() || !get_account_address_from_str(addr_info, nettype, value)) {
|
|
if (reason) {
|
|
if (value.empty())
|
|
*reason =
|
|
"The value={}, mapping into the wallet address, specifies a wallet address of 0 length"_format(
|
|
value);
|
|
else
|
|
*reason =
|
|
"Could not convert the wallet address string, check it is correct, value={}"_format(
|
|
value);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Validate blob contents and generate the binary form if possible
|
|
if (blob) {
|
|
auto iter = blob->buffer.begin();
|
|
uint8_t identifier = 0;
|
|
if (addr_info.is_subaddress) {
|
|
identifier |= ONS_WALLET_TYPE_SUBADDRESS;
|
|
} else if (addr_info.has_payment_id) {
|
|
identifier |= ONS_WALLET_TYPE_INTEGRATED;
|
|
}
|
|
iter = std::copy_n(&identifier, 1, iter);
|
|
iter = std::copy_n(
|
|
addr_info.address.m_spend_public_key.data(),
|
|
addr_info.address.m_spend_public_key.size(),
|
|
iter);
|
|
iter = std::copy_n(
|
|
addr_info.address.m_view_public_key.data(),
|
|
addr_info.address.m_view_public_key.size(),
|
|
iter);
|
|
|
|
size_t counter = 65;
|
|
assert(std::distance(blob->buffer.begin(), iter) == static_cast<int>(counter));
|
|
if (addr_info.has_payment_id) {
|
|
std::copy_n(addr_info.payment_id.data(), addr_info.payment_id.size(), iter);
|
|
counter += sizeof(addr_info.payment_id);
|
|
}
|
|
|
|
blob->len = counter;
|
|
}
|
|
} else if (is_lokinet_type(type)) {
|
|
// We need a 52 char base32z string that decodes to a 32-byte value, which really means we
|
|
// need 51 base32z chars (=255 bits) followed by a 1-bit value ('y'=0, or 'o'=0b10000);
|
|
// anything else in the last spot isn't a valid lokinet address.
|
|
if (check_condition(
|
|
value.size() != 57 || !tools::ends_with(value, ".loki") ||
|
|
!oxenc::is_base32z(value.substr(0, 52)) ||
|
|
!(value[51] == 'y' || value[51] == 'o'),
|
|
reason,
|
|
"'{}' is not a valid lokinet address",
|
|
value))
|
|
return false;
|
|
|
|
if (blob) {
|
|
blob->len = sizeof(crypto::ed25519_public_key);
|
|
oxenc::from_base32z(value.begin(), value.begin() + 52, blob->buffer.begin());
|
|
}
|
|
} else {
|
|
assert(type == mapping_type::session);
|
|
// NOTE: Check value is hex of the right size
|
|
if (check_condition(
|
|
value.size() != 2 * SESSION_PUBLIC_KEY_BINARY_LENGTH,
|
|
reason,
|
|
"The value={} is not the required {}-character hex string session public key, "
|
|
"length={}",
|
|
value,
|
|
2 * SESSION_PUBLIC_KEY_BINARY_LENGTH,
|
|
value.size()))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!oxenc::is_hex(value),
|
|
reason,
|
|
"value={} specifies name -> value mapping where the value is not a hex string",
|
|
value))
|
|
return false;
|
|
|
|
// NOTE: Session public keys are 33 bytes, with the first byte being 0x05 and the remaining
|
|
// 32 being the public key.
|
|
if (check_condition(
|
|
!tools::starts_with(value, "05"),
|
|
reason,
|
|
"ONS type=session specifies mapping from name -> ed25519 key where the key is "
|
|
"not prefixed with 05, given ed25519={}",
|
|
value))
|
|
return false;
|
|
|
|
if (blob) // NOTE: Given blob, write the binary output
|
|
{
|
|
blob->len = value.size() / 2;
|
|
assert(blob->len <= blob->buffer.size());
|
|
oxenc::from_hex(value.begin(), value.end(), blob->buffer.begin());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static_assert(
|
|
SODIUM_ENCRYPTION_EXTRA_BYTES ==
|
|
crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
static_assert(SODIUM_ENCRYPTION_EXTRA_BYTES >= crypto_secretbox_MACBYTES);
|
|
bool mapping_value::validate_encrypted(
|
|
mapping_type type, std::string_view value, mapping_value* blob, std::string* reason) {
|
|
if (blob)
|
|
*blob = {};
|
|
|
|
int value_len = crypto_aead_xchacha20poly1305_ietf_ABYTES +
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
|
|
|
if (is_lokinet_type(type))
|
|
value_len += LOKINET_ADDRESS_BINARY_LENGTH;
|
|
else if (type == mapping_type::wallet) {
|
|
value_len = crypto_aead_xchacha20poly1305_ietf_ABYTES +
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; // Add the length in check_length
|
|
} else if (type == mapping_type::session) {
|
|
value_len += SESSION_PUBLIC_KEY_BINARY_LENGTH;
|
|
|
|
// Allow an HF15 argon2 encrypted value which doesn't contain a nonce:
|
|
if (value.size() == value_len - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES)
|
|
value_len -= crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
|
} else {
|
|
if (reason)
|
|
*reason = "Unhandled type passed into {}"_format(__func__);
|
|
return false;
|
|
}
|
|
|
|
if (!check_lengths(type, value, value_len, true /*binary_val*/, reason))
|
|
return false;
|
|
|
|
if (blob) {
|
|
blob->len = value.size();
|
|
std::memcpy(blob->buffer.data(), value.data(), value.size());
|
|
blob->encrypted = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
mapping_value::mapping_value(std::string encrypted_value, std::string nonce) : buffer{0} {
|
|
auto it = std::copy(encrypted_value.begin(), encrypted_value.end(), buffer.begin());
|
|
std::copy(nonce.begin(), nonce.end(), it);
|
|
len = encrypted_value.size() + nonce.size();
|
|
encrypted = true;
|
|
}
|
|
|
|
mapping_value::mapping_value() : buffer{0}, encrypted(false), len(0) {}
|
|
|
|
std::string name_hash_bytes_to_base64(std::string_view bytes) {
|
|
if (bytes.size() != NAME_HASH_SIZE)
|
|
throw std::runtime_error{"Invalid name hash: expected exactly 32 bytes"};
|
|
return oxenc::to_base64(bytes);
|
|
}
|
|
|
|
std::optional<std::string> name_hash_input_to_base64(std::string_view input) {
|
|
if (input.size() == NAME_HASH_SIZE)
|
|
return name_hash_bytes_to_base64(input);
|
|
if (input.size() == 2 * NAME_HASH_SIZE && oxenc::is_hex(input))
|
|
return name_hash_bytes_to_base64(oxenc::from_hex(input));
|
|
if (input.size() >= NAME_HASH_SIZE_B64_MIN && input.size() <= NAME_HASH_SIZE_B64_MAX &&
|
|
oxenc::is_base64(input)) {
|
|
std::string tmp = oxenc::from_base64(input);
|
|
if (tmp.size() == NAME_HASH_SIZE) // Could still be off from too much/too little padding
|
|
return name_hash_bytes_to_base64(tmp);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static std::string hash_to_base64(crypto::hash const& hash) {
|
|
return name_hash_bytes_to_base64(tools::view_guts(hash));
|
|
}
|
|
|
|
static bool verify_ons_signature(
|
|
crypto::hash const& hash,
|
|
ons::generic_signature const& signature,
|
|
ons::generic_owner const& owner) {
|
|
if (!owner || !signature)
|
|
return false;
|
|
if (owner.type != signature.type)
|
|
return false;
|
|
if (signature.type == ons::generic_owner_sig_type::monero) {
|
|
return crypto::check_signature(
|
|
hash, owner.wallet.address.m_spend_public_key, signature.monero);
|
|
} else {
|
|
return (crypto_sign_verify_detached(
|
|
signature.data, hash.data(), hash.size(), owner.ed25519.data()) == 0);
|
|
}
|
|
}
|
|
|
|
static bool validate_against_previous_mapping(
|
|
ons::name_system_db& ons_db,
|
|
uint64_t blockchain_height,
|
|
cryptonote::transaction const& tx,
|
|
cryptonote::tx_extra_oxen_name_system const& ons_extra,
|
|
std::string* reason) {
|
|
crypto::hash expected_prev_txid{};
|
|
std::string name_hash = hash_to_base64(ons_extra.name_hash);
|
|
ons::mapping_record mapping = ons_db.get_mapping(ons_extra.type, name_hash);
|
|
|
|
if (ons_extra.is_updating()) {
|
|
// Updating: the mapping must exist and be active, the updated fields must actually change
|
|
// from the current value, and a valid signature over the updated values must be present.
|
|
|
|
if (check_condition(
|
|
!mapping,
|
|
reason,
|
|
"{}, {} update requested but mapping does not exist.",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
if (check_condition(
|
|
!mapping.active(blockchain_height),
|
|
reason,
|
|
"{}, {} TX requested to update mapping that has already expired",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
expected_prev_txid = mapping.txid;
|
|
|
|
constexpr auto SPECIFYING_SAME_VALUE_ERR =
|
|
"{}, {} field to update is specifying the same mapping {}"sv;
|
|
if (check_condition(
|
|
ons_extra.field_is_set(ons::extra_field::encrypted_value) &&
|
|
ons_extra.encrypted_value == mapping.encrypted_value.to_view(),
|
|
reason,
|
|
SPECIFYING_SAME_VALUE_ERR,
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra),
|
|
"value"))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
ons_extra.field_is_set(ons::extra_field::owner) &&
|
|
ons_extra.owner == mapping.owner,
|
|
reason,
|
|
SPECIFYING_SAME_VALUE_ERR,
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra),
|
|
"owner"))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
ons_extra.field_is_set(ons::extra_field::backup_owner) &&
|
|
ons_extra.backup_owner == mapping.backup_owner,
|
|
reason,
|
|
SPECIFYING_SAME_VALUE_ERR,
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra),
|
|
"backup_owner"))
|
|
return false;
|
|
|
|
// Validate signature
|
|
auto data = tx_extra_signature(
|
|
ons_extra.encrypted_value,
|
|
ons_extra.field_is_set(ons::extra_field::owner) ? &ons_extra.owner : nullptr,
|
|
ons_extra.field_is_set(ons::extra_field::backup_owner) ? &ons_extra.backup_owner
|
|
: nullptr,
|
|
expected_prev_txid);
|
|
if (check_condition(
|
|
data.empty(),
|
|
reason,
|
|
"{}, {} unexpectedly failed to generate signature, please inform the Oxen "
|
|
"developers",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
|
|
crypto::hash hash;
|
|
crypto_generichash(
|
|
hash.data(),
|
|
hash.size(),
|
|
reinterpret_cast<const unsigned char*>(data.data()),
|
|
data.size(),
|
|
nullptr /*key*/,
|
|
0 /*key_len*/);
|
|
|
|
if (check_condition(
|
|
!verify_ons_signature(hash, ons_extra.signature, mapping.owner) &&
|
|
!verify_ons_signature(hash, ons_extra.signature, mapping.backup_owner),
|
|
reason,
|
|
"{}, {} failed to verify signature for ONS update, current owner={}, backup "
|
|
"owner={}",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra),
|
|
mapping.owner.to_string(ons_db.network_type()),
|
|
mapping.backup_owner.to_string(ons_db.network_type())))
|
|
return false;
|
|
} else if (ons_extra.is_buying()) {
|
|
// If buying a new name then the existing name must not be active
|
|
if (check_condition(
|
|
mapping.active(blockchain_height),
|
|
reason,
|
|
"Cannot buy an ONS name that is already registered: name_hash={}, type={}; TX: "
|
|
"{}; {}",
|
|
mapping.name_hash,
|
|
mapping.type,
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
|
|
// If buying a new wallet name then the existing session name must not be active and vice
|
|
// versa The owner of an existing name but different type is allowed to register but the
|
|
// owner and backup owners of the new mapping must be from the same owners and backup owners
|
|
// of the previous mapping ie no new addresses are allowed to be added as owner or backup
|
|
// owner.
|
|
if (ons_extra.type == mapping_type::wallet || ons_extra.type == mapping_type::session) {
|
|
auto buy_type_name = ons_extra.type == mapping_type::wallet ? "wallet"sv : "session"sv;
|
|
auto alt_type_name = ons_extra.type == mapping_type::wallet ? "session"sv : "wallet"sv;
|
|
auto alt_type = ons_extra.type == mapping_type::wallet ? mapping_type::session
|
|
: mapping_type::wallet;
|
|
|
|
ons::mapping_record alt_mapping = ons_db.get_mapping(alt_type, name_hash);
|
|
|
|
auto is_alt_record_owner = [&alt_mapping](const auto& new_owner) {
|
|
return new_owner == alt_mapping.owner || new_owner == alt_mapping.backup_owner;
|
|
};
|
|
|
|
if (check_condition(
|
|
alt_mapping.active(blockchain_height) && // alternative mapping exists
|
|
(!is_alt_record_owner(ons_extra.owner) ||
|
|
(ons_extra.field_is_set(ons::extra_field::backup_owner) &&
|
|
!is_alt_record_owner(ons_extra.backup_owner))),
|
|
reason,
|
|
"Cannot buy an ONS {} name that has an already registered {} name: "
|
|
"name_hash={}, type={}; TX: {}; {}",
|
|
buy_type_name,
|
|
alt_type_name,
|
|
mapping.name_hash,
|
|
mapping.type,
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
}
|
|
} else if (ons_extra.is_renewing()) {
|
|
// We allow anyone to renew a name, but it has to exist and be currently active
|
|
if (check_condition(
|
|
!mapping,
|
|
reason,
|
|
"{}, {} renewal requested but mapping does not exist.",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
if (check_condition(
|
|
!mapping.active(blockchain_height),
|
|
reason,
|
|
"{}, {} TX requested to renew mapping that has already expired",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra)))
|
|
return false;
|
|
expected_prev_txid = mapping.txid;
|
|
} else {
|
|
check_condition(
|
|
true,
|
|
reason,
|
|
"{}, {} is not a valid buy, update, or renew ONS tx",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra));
|
|
return false;
|
|
}
|
|
|
|
if (check_condition(
|
|
ons_extra.prev_txid != expected_prev_txid,
|
|
reason,
|
|
"{}, {} specified prior txid {} but expected {}; perhaps a competing ONS TX was "
|
|
"submitted and accepted before this ONS update TX was processed?",
|
|
tx,
|
|
ons_extra_string(ons_db.network_type(), ons_extra),
|
|
ons_extra.prev_txid,
|
|
expected_prev_txid))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Sanity check value to disallow the empty name hash
|
|
static const crypto::hash null_name_hash = name_to_hash("");
|
|
|
|
bool name_system_db::validate_ons_tx(
|
|
hf hf_version,
|
|
uint64_t blockchain_height,
|
|
cryptonote::transaction const& tx,
|
|
cryptonote::tx_extra_oxen_name_system& ons_extra,
|
|
std::string* reason) {
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Pull out ONS Extra from TX
|
|
// -----------------------------------------------------------------------------------------------
|
|
{
|
|
if (check_condition(
|
|
tx.type != cryptonote::txtype::oxen_name_system,
|
|
reason,
|
|
"{} uses wrong tx type, expected={}",
|
|
tx,
|
|
cryptonote::txtype::oxen_name_system))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!cryptonote::get_field_from_tx_extra(tx.extra, ons_extra),
|
|
reason,
|
|
"{} didn't have oxen name service in the tx_extra",
|
|
tx))
|
|
return false;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Check TX ONS Serialized Fields are NULL if they are not specified
|
|
// -----------------------------------------------------------------------------------------------
|
|
{
|
|
constexpr auto VALUE_SPECIFIED_BUT_NOT_REQUESTED =
|
|
"{}, {} given field {} but field is not requested to be serialised"sv;
|
|
if (check_condition(
|
|
!ons_extra.field_is_set(ons::extra_field::encrypted_value) &&
|
|
ons_extra.encrypted_value.size(),
|
|
reason,
|
|
VALUE_SPECIFIED_BUT_NOT_REQUESTED,
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
"encrypted_value"))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!ons_extra.field_is_set(ons::extra_field::owner) && ons_extra.owner,
|
|
reason,
|
|
VALUE_SPECIFIED_BUT_NOT_REQUESTED,
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
"owner"))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!ons_extra.field_is_set(ons::extra_field::backup_owner) &&
|
|
ons_extra.backup_owner,
|
|
reason,
|
|
VALUE_SPECIFIED_BUT_NOT_REQUESTED,
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
"backup_owner"))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!ons_extra.field_is_set(ons::extra_field::signature) && ons_extra.signature,
|
|
reason,
|
|
VALUE_SPECIFIED_BUT_NOT_REQUESTED,
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
"signature"))
|
|
return false;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Simple ONS Extra Validation
|
|
// -----------------------------------------------------------------------------------------------
|
|
{
|
|
if (check_condition(
|
|
ons_extra.version != 0,
|
|
reason,
|
|
"{}, {} unexpected version={:d}, expected 0",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
ons_extra.version))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
!ons::mapping_type_allowed(hf_version, ons_extra.type),
|
|
reason,
|
|
"{}, {} specifying type={} is disallowed in HF{:d}",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
ons_extra.type,
|
|
static_cast<uint8_t>(hf_version)))
|
|
return false;
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Serialized Values Check
|
|
// -----------------------------------------------------------------------------------------------
|
|
if (check_condition(
|
|
!ons_extra.is_buying() && !ons_extra.is_updating() && !ons_extra.is_renewing(),
|
|
reason,
|
|
"{}, {} TX extra does not specify valid combination of bits for serialized "
|
|
"fields={}",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
std::bitset<sizeof(ons_extra.fields) * 8>(static_cast<size_t>(ons_extra.fields))
|
|
.to_string()))
|
|
return false;
|
|
|
|
if (check_condition(
|
|
ons_extra.field_is_set(ons::extra_field::owner) &&
|
|
ons_extra.field_is_set(ons::extra_field::backup_owner) &&
|
|
ons_extra.owner == ons_extra.backup_owner,
|
|
reason,
|
|
"{}, {} specifying owner the same as the backup owner={}",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
ons_extra.backup_owner.to_string(nettype))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// ONS Field(s) Validation
|
|
// -----------------------------------------------------------------------------------------------
|
|
{
|
|
if (check_condition(
|
|
(ons_extra.name_hash == null_name_hash || !ons_extra.name_hash),
|
|
reason,
|
|
"{}, {} specified the null name hash",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra)))
|
|
return false;
|
|
|
|
if (ons_extra.field_is_set(ons::extra_field::encrypted_value)) {
|
|
if (!mapping_value::validate_encrypted(
|
|
ons_extra.type, ons_extra.encrypted_value, nullptr, reason))
|
|
return false;
|
|
}
|
|
|
|
if (!validate_against_previous_mapping(*this, blockchain_height, tx, ons_extra, reason))
|
|
return false;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Burn Validation
|
|
// -----------------------------------------------------------------------------------------------
|
|
{
|
|
uint64_t burn = cryptonote::get_burned_amount_from_tx_extra(tx.extra);
|
|
uint64_t const burn_required = (ons_extra.is_buying() || ons_extra.is_renewing())
|
|
? burn_needed(hf_version, ons_extra.type)
|
|
: 0;
|
|
if (hf_version == hf::hf18 && burn > burn_required && blockchain_height < 524'000) {
|
|
// Testnet sync fix: PR #1433 merged that lowered fees for HF18 while testnet was
|
|
// already on HF18, but broke syncing because earlier HF18 blocks have ONS txes at the
|
|
// higher fees, so this allows them to pass by pretending the tx burned the right
|
|
// amount.
|
|
burn = burn_required;
|
|
}
|
|
|
|
if (check_condition(
|
|
burn != burn_required,
|
|
reason,
|
|
"{}, {} burned {} OXEN={}, required={}",
|
|
tx,
|
|
ons_extra_string(nettype, ons_extra),
|
|
burn > burn_required ? "too much" : "insufficient",
|
|
burn,
|
|
burn_required))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool validate_mapping_type(
|
|
std::string_view mapping_type_str,
|
|
hf hf_version,
|
|
ons_tx_type txtype,
|
|
ons::mapping_type* mapping_type,
|
|
std::string* reason) {
|
|
std::string mapping = tools::lowercase_ascii_string(mapping_type_str);
|
|
std::optional<ons::mapping_type> mapping_type_;
|
|
if (txtype != ons_tx_type::renew && tools::string_iequal(mapping, "session"))
|
|
mapping_type_ = ons::mapping_type::session;
|
|
else if (hf_version >= hf::hf16_pulse) {
|
|
if (tools::string_iequal(mapping, "lokinet"))
|
|
mapping_type_ = ons::mapping_type::lokinet;
|
|
else if (txtype == ons_tx_type::buy || txtype == ons_tx_type::renew) {
|
|
if (tools::string_iequal_any(
|
|
mapping, "lokinet_1y", "lokinet_1years")) // Can also specify "lokinet"
|
|
mapping_type_ = ons::mapping_type::lokinet;
|
|
else if (tools::string_iequal_any(mapping, "lokinet_2y", "lokinet_2years"))
|
|
mapping_type_ = ons::mapping_type::lokinet_2years;
|
|
else if (tools::string_iequal_any(mapping, "lokinet_5y", "lokinet_5years"))
|
|
mapping_type_ = ons::mapping_type::lokinet_5years;
|
|
else if (tools::string_iequal_any(mapping, "lokinet_10y", "lokinet_10years"))
|
|
mapping_type_ = ons::mapping_type::lokinet_10years;
|
|
}
|
|
}
|
|
if (hf_version >= hf::hf18) {
|
|
if (tools::string_iequal(mapping, "wallet"))
|
|
mapping_type_ = ons::mapping_type::wallet;
|
|
}
|
|
|
|
if (!mapping_type_) {
|
|
if (reason)
|
|
*reason = "Unsupported ONS type \"" + std::string{mapping_type_str} + "\"; supported " +
|
|
(txtype == ons_tx_type::update ? "update types are: session, lokinet, wallet"
|
|
: txtype == ons_tx_type::renew ? "renew types are: lokinet_1y, lokinet_2y, "
|
|
"lokinet_5y, lokinet_10y"
|
|
: txtype == ons_tx_type::buy ? "buy types are session, lokinet_1y, "
|
|
"lokinet_2y, lokinet_5y, lokinet_10y"
|
|
: "lookup types are session, lokinet, wallet");
|
|
return false;
|
|
}
|
|
|
|
if (mapping_type)
|
|
*mapping_type = *mapping_type_;
|
|
return true;
|
|
}
|
|
|
|
crypto::hash name_to_hash(std::string_view name, const std::optional<crypto::hash>& key) {
|
|
assert(std::none_of(name.begin(), name.end(), [](char c) { return std::isupper(c); }));
|
|
crypto::hash result = {};
|
|
static_assert(
|
|
sizeof(result) >= crypto_generichash_BYTES,
|
|
"Sodium can generate arbitrary length hashes, but recommend the minimum size for a "
|
|
"secure hash must be >= crypto_generichash_BYTES");
|
|
crypto_generichash_blake2b(
|
|
result.data(),
|
|
result.size(),
|
|
reinterpret_cast<const unsigned char*>(name.data()),
|
|
static_cast<unsigned long long>(name.size()),
|
|
key ? key->data() : nullptr,
|
|
key ? key->size() : 0);
|
|
return result;
|
|
}
|
|
|
|
std::string name_to_base64_hash(std::string_view name) {
|
|
crypto::hash hash = name_to_hash(name);
|
|
std::string result = hash_to_base64(hash);
|
|
return result;
|
|
}
|
|
|
|
struct alignas(size_t) secretbox_secret_key {
|
|
unsigned char data[crypto_aead_xchacha20poly1305_ietf_KEYBYTES];
|
|
|
|
secretbox_secret_key& operator=(const crypto::hash& h) {
|
|
static_assert(
|
|
sizeof(secretbox_secret_key::data) == crypto_aead_xchacha20poly1305_ietf_KEYBYTES);
|
|
static_assert(sizeof(secretbox_secret_key::data) == crypto::hash::size());
|
|
std::memcpy(data, h.data(), sizeof(data));
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
// New (8.x):
|
|
// We encrypt using xchacha20-poly1305; for the encryption key we use the (secret) keyed hash:
|
|
// H(name, key=H(name)). Note that H(name) is public info but this keyed hash is known only to the
|
|
// resolver.
|
|
//
|
|
// Note that the name must *already* be lower-cased (we do not transform or validate that here).
|
|
//
|
|
// If the name hash is already available then it can be passed by pointer as the second argument,
|
|
// otherwise pass nullptr to calculate the hash when needed. (Note that name_hash is not used when
|
|
// heavy=true).
|
|
static void name_to_encryption_key(
|
|
std::string_view name, const crypto::hash* name_hash, secretbox_secret_key& out) {
|
|
static_assert(
|
|
sizeof(out) == crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
|
"Encrypting key needs to have sufficient space for running encryption functions via "
|
|
"libsodium");
|
|
|
|
crypto::hash name_hash_;
|
|
if (!name_hash)
|
|
name_hash = &(name_hash_ = name_to_hash(name));
|
|
|
|
out = name_to_hash(name, *name_hash);
|
|
}
|
|
|
|
// Old (7.x) "heavy" encryption:
|
|
//
|
|
// We encrypt using the older xsalsa20-poly1305 encryption scheme, and for encryption key we use an
|
|
// expensive argon2 "moderate" hash of the name (with null salt).
|
|
static constexpr unsigned char OLD_ENC_SALT[crypto_pwhash_SALTBYTES] = {};
|
|
static bool name_to_encryption_key_argon2(std::string_view name, secretbox_secret_key& out) {
|
|
static_assert(
|
|
sizeof(out) == crypto_secretbox_KEYBYTES,
|
|
"Encrypting key needs to have sufficient space for running encryption functions via "
|
|
"libsodium");
|
|
return 0 == crypto_pwhash(
|
|
out.data,
|
|
sizeof(out.data),
|
|
name.data(),
|
|
name.size(),
|
|
OLD_ENC_SALT,
|
|
crypto_pwhash_OPSLIMIT_MODERATE,
|
|
crypto_pwhash_MEMLIMIT_MODERATE,
|
|
crypto_pwhash_ALG_ARGON2ID13);
|
|
}
|
|
|
|
bool mapping_value::encrypt(
|
|
std::string_view name, const crypto::hash* name_hash, bool deprecated_heavy) {
|
|
assert(!encrypted);
|
|
if (encrypted)
|
|
return false;
|
|
|
|
assert(std::none_of(name.begin(), name.end(), [](char c) { return std::isupper(c); }));
|
|
|
|
size_t const encryption_len =
|
|
len + (deprecated_heavy ? crypto_secretbox_MACBYTES
|
|
: crypto_aead_xchacha20poly1305_ietf_ABYTES +
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
|
|
if (encryption_len > buffer.size()) {
|
|
log::error(
|
|
logcat,
|
|
"Encrypted value pre-allocated buffer too small={}, required={}",
|
|
buffer.size(),
|
|
encryption_len);
|
|
return false;
|
|
}
|
|
|
|
decltype(buffer) enc_buffer;
|
|
secretbox_secret_key skey;
|
|
if (deprecated_heavy) {
|
|
if (name_to_encryption_key_argon2(name, skey))
|
|
encrypted =
|
|
(crypto_secretbox_easy(
|
|
enc_buffer.data(),
|
|
buffer.data(),
|
|
len,
|
|
OLD_ENCRYPTION_NONCE,
|
|
skey.data) == 0);
|
|
} else {
|
|
name_to_encryption_key(name, name_hash, skey);
|
|
unsigned long long actual_length;
|
|
|
|
// Create a random nonce:
|
|
auto* nonce = &enc_buffer[encryption_len - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES];
|
|
randombytes_buf(nonce, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
|
|
encrypted = 0 == crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
&enc_buffer[0],
|
|
&actual_length,
|
|
&buffer[0],
|
|
len,
|
|
nullptr,
|
|
0, // additional data
|
|
nullptr, // nsec, always nullptr according to libsodium docs (just
|
|
// here for API compat)
|
|
nonce,
|
|
skey.data);
|
|
|
|
if (encrypted)
|
|
assert(actual_length == encryption_len - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
|
}
|
|
|
|
if (encrypted) {
|
|
len = encryption_len;
|
|
buffer = enc_buffer;
|
|
}
|
|
return encrypted;
|
|
}
|
|
|
|
bool mapping_value::decrypt(
|
|
std::string_view name, mapping_type type, const crypto::hash* name_hash) {
|
|
assert(encrypted);
|
|
if (!encrypted)
|
|
return false;
|
|
|
|
assert(std::none_of(name.begin(), name.end(), [](char c) { return std::isupper(c); }));
|
|
|
|
size_t dec_length;
|
|
decltype(buffer) dec_buffer;
|
|
secretbox_secret_key skey;
|
|
|
|
// Check for an old-style, argon2-based encryption, used before HF16. (After HF16 we use a much
|
|
// faster blake2b-hashed key, and a random nonce appended to the end.)
|
|
if (type == mapping_type::session &&
|
|
len == SESSION_PUBLIC_KEY_BINARY_LENGTH + crypto_secretbox_MACBYTES) {
|
|
dec_length = SESSION_PUBLIC_KEY_BINARY_LENGTH;
|
|
encrypted =
|
|
!(name_to_encryption_key_argon2(name, skey) && 0 == crypto_secretbox_open_easy(
|
|
dec_buffer.data(),
|
|
buffer.data(),
|
|
len,
|
|
OLD_ENCRYPTION_NONCE,
|
|
skey.data));
|
|
} else {
|
|
switch (type) {
|
|
case mapping_type::session: dec_length = SESSION_PUBLIC_KEY_BINARY_LENGTH; break;
|
|
case mapping_type::lokinet: dec_length = LOKINET_ADDRESS_BINARY_LENGTH; break;
|
|
case mapping_type::wallet: // Wallet type has variable type, check performed in
|
|
// check_length
|
|
if (auto plain_len = len - crypto_aead_xchacha20poly1305_ietf_ABYTES -
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
|
plain_len == WALLET_ACCOUNT_BINARY_LENGTH_INC_PAYMENT_ID ||
|
|
plain_len == WALLET_ACCOUNT_BINARY_LENGTH_NO_PAYMENT_ID) {
|
|
dec_length = plain_len;
|
|
} else {
|
|
log::error(
|
|
logcat,
|
|
"Invalid wallet mapping_type length passed to mapping_value::decrypt");
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
log::error(logcat, "Invalid mapping_type passed to mapping_value::decrypt");
|
|
return false;
|
|
}
|
|
|
|
auto expected_len = dec_length + crypto_aead_xchacha20poly1305_ietf_ABYTES +
|
|
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
|
if (len != expected_len) {
|
|
log::error(
|
|
logcat, "Encrypted value size is invalid={}, expected={}", len, expected_len);
|
|
return false;
|
|
}
|
|
const auto& [enc, nonce] = value_nonce(type);
|
|
|
|
name_to_encryption_key(name, name_hash, skey);
|
|
unsigned long long actual_length;
|
|
encrypted =
|
|
!(0 == crypto_aead_xchacha20poly1305_ietf_decrypt(
|
|
dec_buffer.data(),
|
|
&actual_length,
|
|
nullptr, // nsec (always null for this algo)
|
|
enc.data(),
|
|
enc.size(),
|
|
nullptr,
|
|
0, // additional data
|
|
nonce.data(),
|
|
skey.data));
|
|
|
|
if (!encrypted)
|
|
assert(actual_length == dec_length);
|
|
}
|
|
|
|
if (!encrypted) // i.e. decryption success
|
|
{
|
|
len = dec_length;
|
|
buffer = dec_buffer;
|
|
}
|
|
return !encrypted;
|
|
}
|
|
|
|
mapping_value mapping_value::make_encrypted(
|
|
std::string_view name, const crypto::hash* name_hash, bool deprecated_heavy) const {
|
|
mapping_value result{*this};
|
|
result.encrypt(name, name_hash, deprecated_heavy);
|
|
assert(result.encrypted);
|
|
return result;
|
|
}
|
|
|
|
mapping_value mapping_value::make_decrypted(
|
|
std::string_view name, const crypto::hash* name_hash) const {
|
|
mapping_value result{*this};
|
|
result.encrypt(name, name_hash);
|
|
assert(!result.encrypted);
|
|
return result;
|
|
}
|
|
|
|
std::optional<cryptonote::address_parse_info> mapping_value::get_wallet_address_info() const {
|
|
assert(!encrypted);
|
|
if (encrypted)
|
|
return std::nullopt;
|
|
|
|
cryptonote::address_parse_info addr_info{};
|
|
auto* bufpos = &buffer[1];
|
|
std::memcpy(addr_info.address.m_spend_public_key.data(), bufpos, 32);
|
|
bufpos += 32;
|
|
std::memcpy(addr_info.address.m_view_public_key.data(), bufpos, 32);
|
|
if (buffer[0] == ONS_WALLET_TYPE_INTEGRATED) {
|
|
bufpos += 32;
|
|
std::copy_n(bufpos, 8, addr_info.payment_id.data());
|
|
addr_info.has_payment_id = true;
|
|
} else if (buffer[0] == ONS_WALLET_TYPE_SUBADDRESS) {
|
|
addr_info.is_subaddress = true;
|
|
} else
|
|
assert(buffer[0] == ONS_WALLET_TYPE_PRIMARY);
|
|
return addr_info;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool build_default_tables(name_system_db& ons_db) {
|
|
std::string mappings_columns = R"(
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
type INTEGER NOT NULL,
|
|
name_hash VARCHAR NOT NULL,
|
|
encrypted_value BLOB NOT NULL,
|
|
txid BLOB NOT NULL,
|
|
owner_id INTEGER NOT NULL REFERENCES owner(id),
|
|
backup_owner_id INTEGER REFERENCES owner(id),
|
|
update_height INTEGER NOT NULL,
|
|
expiration_height INTEGER
|
|
)";
|
|
|
|
const std::string BUILD_TABLE_SQL = R"(
|
|
CREATE TABLE IF NOT EXISTS owner(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
address BLOB NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
top_height INTEGER NOT NULL,
|
|
top_hash VARCHAR NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
pruned_height INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mappings ()" + mappings_columns +
|
|
R"();
|
|
CREATE INDEX IF NOT EXISTS owner_id_index ON mappings(owner_id);
|
|
DROP INDEX IF EXISTS backup_owner_id_index;
|
|
CREATE INDEX IF NOT EXISTS backup_owner_index ON mappings(backup_owner_id);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS name_type_update ON mappings (name_hash, type, update_height DESC);
|
|
CREATE INDEX IF NOT EXISTS mapping_type_name_exp ON mappings (type, name_hash, expiration_height DESC);
|
|
)";
|
|
|
|
char* table_err_msg = nullptr;
|
|
int table_created = sqlite3_exec(
|
|
ons_db.db,
|
|
BUILD_TABLE_SQL.c_str(),
|
|
nullptr /*callback*/,
|
|
nullptr /*callback context*/,
|
|
&table_err_msg);
|
|
if (table_created != SQLITE_OK) {
|
|
log::error(
|
|
logcat,
|
|
"Can not generate SQL table for ONS: {}",
|
|
(table_err_msg ? table_err_msg : "??"));
|
|
sqlite3_free(table_err_msg);
|
|
return false;
|
|
}
|
|
|
|
// In Loki 8 we dropped some columns that are no longer needed, but SQLite can't do this
|
|
// easily: instead we have to manually recreate the table, so check it and see if the
|
|
// prev_txid or register_height columns still exist: if so, we need to recreate.
|
|
bool need_mappings_migration = false;
|
|
{
|
|
sql_compiled_statement mappings_info{ons_db};
|
|
mappings_info.compile("PRAGMA table_info(mappings)", false);
|
|
while (step(mappings_info) == SQLITE_ROW) {
|
|
auto name = get<std::string_view>(mappings_info, 1);
|
|
if (name == "prev_txid" || name == "register_height") {
|
|
need_mappings_migration = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (need_mappings_migration) {
|
|
// Earlier version migration: we need "update_height" to exist (if this fails it's
|
|
// fine).
|
|
sqlite3_exec(
|
|
ons_db.db,
|
|
"ALTER TABLE mappings ADD COLUMN update_height INTEGER NOT NULL DEFAULT "
|
|
"register_height",
|
|
nullptr /*callback*/,
|
|
nullptr /*callback ctx*/,
|
|
nullptr /*errstr*/);
|
|
|
|
log::info(logcat, "Migrating ONS mappings database to new format");
|
|
const std::string migrate = R"(
|
|
BEGIN TRANSACTION;
|
|
ALTER TABLE mappings RENAME TO mappings_old;
|
|
CREATE TABLE mappings ()" + mappings_columns +
|
|
R"();
|
|
INSERT INTO mappings
|
|
SELECT id, type, name_hash, encrypted_value, txid, owner_id, backup_owner_id, update_height, NULL
|
|
FROM mappings_old;
|
|
DROP TABLE mappings_old;
|
|
CREATE UNIQUE INDEX name_type_update ON mappings(name_hash, type, update_height DESC);
|
|
CREATE INDEX owner_id_index ON mappings(owner_id);
|
|
CREATE INDEX backup_owner_index ON mappings(backup_owner_id);
|
|
CREATE INDEX mapping_type_name_exp ON mappings(type, name_hash, expiration_height DESC);
|
|
COMMIT TRANSACTION;
|
|
)";
|
|
|
|
int migrated = sqlite3_exec(
|
|
ons_db.db,
|
|
migrate.c_str(),
|
|
nullptr /*callback*/,
|
|
nullptr /*callback context*/,
|
|
&table_err_msg);
|
|
if (migrated != SQLITE_OK) {
|
|
log::error(
|
|
logcat,
|
|
"Can not migrate SQL mappings table for ONS: {}",
|
|
(table_err_msg ? table_err_msg : "??"));
|
|
sqlite3_free(table_err_msg);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Updates to add columns; we ignore errors on these since they will fail if the column
|
|
// already exists
|
|
for (const auto& upgrade : {
|
|
"ALTER TABLE settings ADD COLUMN pruned_height INTEGER NOT NULL DEFAULT 0",
|
|
}) {
|
|
sqlite3_exec(
|
|
ons_db.db,
|
|
upgrade,
|
|
nullptr /*callback*/,
|
|
nullptr /*callback ctx*/,
|
|
nullptr /*errstr*/);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const std::string sql_select_mappings_and_owners_prefix = R"(
|
|
SELECT mappings.*, o1.address, o2.address, MAX(update_height)
|
|
FROM mappings
|
|
JOIN owner o1 ON mappings.owner_id = o1.id
|
|
LEFT JOIN owner o2 ON mappings.backup_owner_id = o2.id
|
|
)"s;
|
|
const std::string sql_select_mappings_and_owners_suffix = " GROUP BY name_hash, type";
|
|
|
|
struct scoped_db_transaction {
|
|
scoped_db_transaction(name_system_db& ons_db);
|
|
~scoped_db_transaction();
|
|
operator bool() const { return initialised; }
|
|
name_system_db& ons_db;
|
|
bool commit = false; // If true, on destruction- END the transaction otherwise ROLLBACK all
|
|
// SQLite events prior for the ons_db
|
|
bool initialised = false;
|
|
};
|
|
|
|
scoped_db_transaction::scoped_db_transaction(name_system_db& ons_db) : ons_db(ons_db) {
|
|
if (ons_db.transaction_begun) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to begin transaction, transaction exists previously that was not "
|
|
"closed properly");
|
|
return;
|
|
}
|
|
|
|
char* sql_err = nullptr;
|
|
if (sqlite3_exec(ons_db.db, "BEGIN;", nullptr, nullptr, &sql_err) != SQLITE_OK) {
|
|
log::error(
|
|
logcat, "Failed to begin transaction , reason={}", (sql_err ? sql_err : "??"));
|
|
sqlite3_free(sql_err);
|
|
return;
|
|
}
|
|
|
|
initialised = true;
|
|
ons_db.transaction_begun = true;
|
|
}
|
|
|
|
scoped_db_transaction::~scoped_db_transaction() {
|
|
if (!initialised)
|
|
return;
|
|
if (!ons_db.transaction_begun) {
|
|
log::error(
|
|
logcat,
|
|
"Trying to apply non-existent transaction (no prior history of a db "
|
|
"transaction beginning) to the ONS DB");
|
|
return;
|
|
}
|
|
|
|
char* sql_err = nullptr;
|
|
if (sqlite3_exec(ons_db.db, commit ? "END;" : "ROLLBACK;", NULL, NULL, &sql_err) !=
|
|
SQLITE_OK) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to {} transaction to ONS DB, reason={}",
|
|
(commit ? "end " : "rollback "),
|
|
(sql_err ? sql_err : "??"));
|
|
sqlite3_free(sql_err);
|
|
return;
|
|
}
|
|
|
|
ons_db.transaction_begun = false;
|
|
}
|
|
|
|
enum struct db_version { v0, v1_track_updates, v2_full_rows };
|
|
auto constexpr DB_VERSION = db_version::v2_full_rows;
|
|
|
|
constexpr auto EXPIRATION = " (expiration_height IS NULL OR expiration_height >= ?) "sv;
|
|
|
|
} // namespace
|
|
|
|
bool name_system_db::init(
|
|
cryptonote::Blockchain const* blockchain, cryptonote::network_type nettype, sqlite3* db) {
|
|
if (!db)
|
|
return false;
|
|
this->db = db;
|
|
this->nettype = nettype;
|
|
|
|
std::string const GET_MAPPINGS_BY_OWNER_STR = sql_select_mappings_and_owners_prefix +
|
|
"WHERE ? IN (o1.address, o2.address)" +
|
|
sql_select_mappings_and_owners_suffix;
|
|
std::string const GET_MAPPING_STR = sql_select_mappings_and_owners_prefix +
|
|
"WHERE type = ? AND name_hash = ?" +
|
|
sql_select_mappings_and_owners_suffix;
|
|
|
|
const std::string GET_MAPPING_COUNTS_STR = R"(
|
|
SELECT type, COUNT(*) FROM (
|
|
SELECT DISTINCT type, name_hash FROM mappings WHERE )" +
|
|
std::string{EXPIRATION} + R"(
|
|
)
|
|
GROUP BY type)";
|
|
|
|
std::string const RESOLVE_STR = R"(
|
|
SELECT encrypted_value, MAX(update_height)
|
|
FROM mappings
|
|
WHERE type = ? AND name_hash = ? AND)" +
|
|
std::string{EXPIRATION};
|
|
|
|
constexpr auto GET_SETTINGS_STR = "SELECT * FROM settings WHERE id = 1"sv;
|
|
constexpr auto GET_OWNER_BY_ID_STR = "SELECT * FROM owner WHERE id = ?"sv;
|
|
constexpr auto GET_OWNER_BY_KEY_STR = "SELECT * FROM owner WHERE address = ?"sv;
|
|
|
|
// Prune queries used when we need to rollback to remove records added after the detach point:
|
|
constexpr auto PRUNE_MAPPINGS_STR = "DELETE FROM mappings WHERE update_height >= ?"sv;
|
|
constexpr auto PRUNE_OWNERS_STR = R"(
|
|
DELETE FROM owner
|
|
WHERE NOT EXISTS (SELECT * FROM mappings WHERE owner.id = mappings.owner_id)
|
|
AND NOT EXISTS (SELECT * FROM mappings WHERE owner.id = mappings.backup_owner_id))"sv;
|
|
|
|
constexpr auto SAVE_MAPPING_STR =
|
|
"INSERT INTO mappings (type, name_hash, encrypted_value, txid, owner_id, backup_owner_id, update_height, expiration_height) VALUES (?,?,?,?,?,?,?,?)"sv;
|
|
constexpr auto SAVE_OWNER_STR = "INSERT INTO owner (address) VALUES (?)"sv;
|
|
constexpr auto SAVE_SETTINGS_STR =
|
|
"INSERT OR REPLACE INTO settings (id, top_height, top_hash, version) VALUES (1,?,?,?)"sv;
|
|
|
|
if (!build_default_tables(*this))
|
|
return false;
|
|
|
|
if (!get_settings_sql.compile(GET_SETTINGS_STR) ||
|
|
!save_settings_sql.compile(SAVE_SETTINGS_STR))
|
|
return false;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Migrate DB
|
|
//
|
|
// No statements (aside from settings) have been prepared yet, since the prepared statements we
|
|
// need may require migration. This code must thus take care to locally execute or prepare
|
|
// whatever statements it needs.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
if (settings_record settings = get_settings()) {
|
|
if (settings.version != static_cast<decltype(settings.version)>(DB_VERSION)) {
|
|
if (!blockchain) {
|
|
log::error(logcat, "Migration required, blockchain can not be nullptr");
|
|
return false;
|
|
}
|
|
|
|
if (blockchain->get_db().is_read_only()) {
|
|
log::error(logcat, "DB is opened in read-only mode, unable to migrate ONS DB");
|
|
return false;
|
|
}
|
|
|
|
scoped_db_transaction db_transaction(*this);
|
|
if (!db_transaction)
|
|
return false;
|
|
|
|
if (settings.version <
|
|
static_cast<decltype(settings.version)>(db_version::v1_track_updates)) {
|
|
|
|
std::vector<mapping_record> all_mappings = {};
|
|
{
|
|
sql_compiled_statement st{*this};
|
|
if (!st.compile(
|
|
sql_select_mappings_and_owners_prefix +
|
|
sql_select_mappings_and_owners_suffix))
|
|
return false;
|
|
sql_run_statement(ons_sql_type::get_mappings, st, &all_mappings);
|
|
}
|
|
|
|
std::vector<crypto::hash> hashes;
|
|
hashes.reserve(all_mappings.size());
|
|
for (mapping_record const& record : all_mappings)
|
|
hashes.push_back(record.txid);
|
|
|
|
constexpr auto UPDATE_MAPPING_HEIGHT =
|
|
"UPDATE mappings SET update_height = ? WHERE id = ?"sv;
|
|
sql_compiled_statement update_mapping_height{*this};
|
|
if (!update_mapping_height.compile(UPDATE_MAPPING_HEIGHT, false))
|
|
return false;
|
|
|
|
std::vector<uint64_t> heights = blockchain->get_transactions_heights(hashes);
|
|
for (size_t i = 0; i < all_mappings.size(); i++) {
|
|
|
|
bind_and_run(
|
|
ons_sql_type::internal_cmd,
|
|
update_mapping_height,
|
|
nullptr,
|
|
heights[i],
|
|
all_mappings[i].id);
|
|
}
|
|
}
|
|
|
|
if (settings.version <
|
|
static_cast<decltype(settings.version)>(db_version::v2_full_rows)) {
|
|
sql_compiled_statement prune_height{*this};
|
|
if (!prune_height.compile(
|
|
"UPDATE settings SET pruned_height = (SELECT MAX(update_height) FROM "
|
|
"mappings)",
|
|
false))
|
|
return false;
|
|
|
|
if (step(prune_height) != SQLITE_DONE)
|
|
return false;
|
|
}
|
|
|
|
save_settings(
|
|
settings.top_height,
|
|
settings.top_hash,
|
|
static_cast<int>(db_version::v2_full_rows));
|
|
db_transaction.commit = true;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Prepare commonly executed sql statements
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
if (!get_mappings_by_owner_sql.compile(GET_MAPPINGS_BY_OWNER_STR) ||
|
|
!get_mapping_sql.compile(GET_MAPPING_STR) ||
|
|
!get_mapping_counts_sql.compile(GET_MAPPING_COUNTS_STR) ||
|
|
!resolve_sql.compile(RESOLVE_STR) || !get_owner_by_id_sql.compile(GET_OWNER_BY_ID_STR) ||
|
|
!get_owner_by_key_sql.compile(GET_OWNER_BY_KEY_STR) ||
|
|
!prune_mappings_sql.compile(PRUNE_MAPPINGS_STR) ||
|
|
!prune_owners_sql.compile(PRUNE_OWNERS_STR) ||
|
|
!save_mapping_sql.compile(SAVE_MAPPING_STR) || !save_owner_sql.compile(SAVE_OWNER_STR)) {
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Check settings
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
if (settings_record settings = get_settings()) {
|
|
if (!blockchain) {
|
|
assert(nettype == cryptonote::network_type::FAKECHAIN);
|
|
return nettype == cryptonote::network_type::FAKECHAIN;
|
|
}
|
|
|
|
uint64_t ons_height = 0;
|
|
crypto::hash ons_hash = blockchain->get_tail_id(ons_height);
|
|
|
|
// Try support out of date ONS databases by checking if the stored
|
|
// settings->[top_hash|top_height] match what we expect. If they match, we
|
|
// don't drop the DB but will load the missing blocks in a later step.
|
|
|
|
cryptonote::block ons_blk = {};
|
|
bool orphan = false;
|
|
if (blockchain->get_block_by_hash(settings.top_hash, ons_blk, &orphan)) {
|
|
bool ons_height_matches = settings.top_height == cryptonote::get_block_height(ons_blk);
|
|
if (ons_height_matches && !orphan) {
|
|
ons_height = settings.top_height;
|
|
ons_hash = settings.top_hash;
|
|
}
|
|
}
|
|
|
|
if (settings.top_height == ons_height && settings.top_hash == ons_hash) {
|
|
this->last_processed_height = settings.top_height;
|
|
this->last_processed_hash = settings.top_hash;
|
|
assert(settings.version == static_cast<int>(DB_VERSION));
|
|
} else {
|
|
// Otherwise we've got something unrecoverable: a top_hash + top_height that are
|
|
// different from what we have in the blockchain, which means the ons db and blockchain
|
|
// are out of sync. This likely means something external changed the lmdb and/or the
|
|
// ons.db, and we can't recover from it: so just drop and recreate the tables completely
|
|
// and rescan from scratch.
|
|
|
|
char constexpr DROP_TABLE_SQL[] =
|
|
"DROP TABLE IF EXISTS owner; DROP TABLE IF EXISTS settings; DROP TABLE IF "
|
|
"EXISTS mappings";
|
|
sqlite3_exec(
|
|
db,
|
|
DROP_TABLE_SQL,
|
|
nullptr /*callback*/,
|
|
nullptr /*callback context*/,
|
|
nullptr);
|
|
if (!build_default_tables(*this))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
name_system_db::~name_system_db() {
|
|
if (!db)
|
|
return;
|
|
|
|
{
|
|
scoped_db_transaction db_transaction(*this);
|
|
save_settings(last_processed_height, last_processed_hash, static_cast<int>(DB_VERSION));
|
|
db_transaction.commit = true;
|
|
}
|
|
|
|
// close_v2 starts shutting down; the actual shutdown occurs once the last prepared statement is
|
|
// finalized (which should happen when the ..._sql members get destructed, right after this).
|
|
sqlite3_close_v2(db);
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::optional<int64_t> add_or_get_owner_id(
|
|
ons::name_system_db& ons_db,
|
|
crypto::hash const& tx_hash,
|
|
cryptonote::tx_extra_oxen_name_system const& entry,
|
|
ons::generic_owner const& key) {
|
|
int64_t result = 0;
|
|
if (owner_record owner = ons_db.get_owner_by_key(key))
|
|
result = owner.id;
|
|
if (result == 0) {
|
|
if (!ons_db.save_owner(key, &result)) {
|
|
log::info(
|
|
logcat,
|
|
"Failed to save ONS owner to DB tx: {}, type: {}, name_hash: {}, owner: {}",
|
|
tx_hash,
|
|
entry.type,
|
|
entry.name_hash,
|
|
entry.owner.to_string(ons_db.network_type()));
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
if (result == 0)
|
|
return std::nullopt;
|
|
return result;
|
|
}
|
|
|
|
// Build a query and bind values that will create a new row at the given height by copying the
|
|
// current highest-height row values and/or updating the given update fields.
|
|
using update_variant = std::variant<uint16_t, int64_t, uint64_t, blob_view, std::string>;
|
|
std::pair<std::string, std::vector<update_variant>> update_record_query(
|
|
name_system_db& ons_db,
|
|
uint64_t height,
|
|
const cryptonote::tx_extra_oxen_name_system& entry,
|
|
const crypto::hash& tx_hash) {
|
|
assert(entry.is_updating() || entry.is_renewing());
|
|
|
|
std::pair<std::string, std::vector<update_variant>> result;
|
|
auto& [sql, bind] = result;
|
|
|
|
sql.reserve(500);
|
|
sql += R"(
|
|
INSERT INTO mappings (type, name_hash, txid, update_height, expiration_height, owner_id, backup_owner_id, encrypted_value)
|
|
SELECT type, name_hash, ?, ?)";
|
|
|
|
bind.emplace_back(blob_view{tx_hash.data(), tx_hash.size()});
|
|
bind.emplace_back(height);
|
|
|
|
constexpr auto suffix =
|
|
" FROM mappings WHERE type = ? AND name_hash = ? ORDER BY update_height DESC LIMIT 1"sv;
|
|
|
|
if (entry.is_renewing()) {
|
|
sql += ", expiration_height + ?, owner_id, backup_owner_id, encrypted_value";
|
|
bind.emplace_back(expiry_blocks(ons_db.network_type(), entry.type).value_or(0));
|
|
} else {
|
|
// Updating
|
|
|
|
sql += ", expiration_height";
|
|
|
|
if (entry.field_is_set(ons::extra_field::owner)) {
|
|
auto opt_id = add_or_get_owner_id(ons_db, tx_hash, entry, entry.owner);
|
|
if (!opt_id) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to add or get owner with key={}",
|
|
entry.owner.to_string(ons_db.network_type()));
|
|
assert(opt_id);
|
|
return {};
|
|
}
|
|
sql += ", ?";
|
|
bind.emplace_back(*opt_id);
|
|
} else
|
|
sql += ", owner_id";
|
|
|
|
if (entry.field_is_set(ons::extra_field::backup_owner)) {
|
|
auto opt_id = add_or_get_owner_id(ons_db, tx_hash, entry, entry.backup_owner);
|
|
if (!opt_id) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to add or get backup owner with key={}",
|
|
entry.backup_owner.to_string(ons_db.network_type()));
|
|
assert(opt_id);
|
|
return {};
|
|
}
|
|
|
|
sql += ", ?";
|
|
bind.emplace_back(*opt_id);
|
|
} else
|
|
sql += ", backup_owner_id";
|
|
|
|
if (entry.field_is_set(ons::extra_field::encrypted_value)) {
|
|
sql += ", ?";
|
|
bind.emplace_back(blob_view{entry.encrypted_value});
|
|
} else
|
|
sql += ", encrypted_value";
|
|
}
|
|
|
|
sql += suffix;
|
|
bind.emplace_back(db_mapping_type(entry.type));
|
|
bind.emplace_back(hash_to_base64(entry.name_hash));
|
|
|
|
return result;
|
|
}
|
|
|
|
bool add_ons_entry(
|
|
ons::name_system_db& ons_db,
|
|
uint64_t height,
|
|
cryptonote::tx_extra_oxen_name_system const& entry,
|
|
crypto::hash const& tx_hash) {
|
|
// -----------------------------------------------------------------------------------------------
|
|
// New Mapping Insert or Completely Replace
|
|
// -----------------------------------------------------------------------------------------------
|
|
if (entry.is_buying()) {
|
|
auto owner_id = add_or_get_owner_id(ons_db, tx_hash, entry, entry.owner);
|
|
if (!owner_id) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to add or get owner with key={}",
|
|
entry.owner.to_string(ons_db.network_type()));
|
|
assert(owner_id);
|
|
return false;
|
|
}
|
|
|
|
std::optional<int64_t> backup_owner_id;
|
|
if (entry.backup_owner) {
|
|
backup_owner_id = add_or_get_owner_id(ons_db, tx_hash, entry, entry.backup_owner);
|
|
if (!backup_owner_id) {
|
|
log::error(
|
|
logcat,
|
|
"Failed to add or get backup owner with key={}",
|
|
entry.backup_owner.to_string(ons_db.network_type()));
|
|
assert(backup_owner_id);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
auto expiry = expiry_blocks(ons_db.network_type(), entry.type);
|
|
if (expiry)
|
|
*expiry += height;
|
|
if (!ons_db.save_mapping(tx_hash, entry, height, expiry, *owner_id, backup_owner_id)) {
|
|
log::info(
|
|
logcat,
|
|
"Failed to save ONS entry to DB tx: {}, type: {}, name_hash: {}, owner: {}",
|
|
tx_hash,
|
|
entry.type,
|
|
entry.name_hash,
|
|
entry.owner.to_string(ons_db.network_type()));
|
|
return false;
|
|
}
|
|
}
|
|
// -----------------------------------------------------------------------------------------------
|
|
// Update mapping or renewal: create a new row copies and updated from the existing top row
|
|
// -----------------------------------------------------------------------------------------------
|
|
else {
|
|
auto [sql, bind] = update_record_query(ons_db, height, entry, tx_hash);
|
|
|
|
if (sql.empty())
|
|
return false; // already MERROR'd
|
|
|
|
// Compile sql statement
|
|
sql_compiled_statement statement{ons_db};
|
|
if (!statement.compile(sql, false /*optimise_for_multiple_usage*/)) {
|
|
log::error(
|
|
logcat, "Failed to compile SQL statement for updating ONS record={}", sql);
|
|
return false;
|
|
}
|
|
|
|
// Bind statement parameters
|
|
bind_container(statement, bind);
|
|
|
|
if (!sql_run_statement(ons_sql_type::save_mapping, statement, nullptr))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool name_system_db::add_block(
|
|
const cryptonote::block& block, const std::vector<cryptonote::transaction>& txs) {
|
|
uint64_t height = cryptonote::get_block_height(block);
|
|
if (last_processed_height >= height)
|
|
return true;
|
|
|
|
scoped_db_transaction db_transaction(*this);
|
|
if (!db_transaction)
|
|
return false;
|
|
|
|
bool ons_parsed_from_block = false;
|
|
if (block.major_version >= hf::hf15_ons) {
|
|
for (cryptonote::transaction const& tx : txs) {
|
|
if (tx.type != cryptonote::txtype::oxen_name_system)
|
|
continue;
|
|
|
|
cryptonote::tx_extra_oxen_name_system entry = {};
|
|
std::string fail_reason;
|
|
if (!validate_ons_tx(block.major_version, height, tx, entry, &fail_reason)) {
|
|
log::error(
|
|
logcat,
|
|
"ONS TX: Failed to validate for tx={}. This should have failed validation "
|
|
"earlier reason={}",
|
|
get_transaction_hash(tx),
|
|
fail_reason);
|
|
assert("Failed to validate acquire name service. Should already have failed "
|
|
"validation prior" == nullptr);
|
|
return false;
|
|
}
|
|
|
|
crypto::hash const& tx_hash = cryptonote::get_transaction_hash(tx);
|
|
if (!add_ons_entry(*this, height, entry, tx_hash))
|
|
return false;
|
|
|
|
ons_parsed_from_block = true;
|
|
}
|
|
}
|
|
|
|
last_processed_height = height;
|
|
last_processed_hash = cryptonote::get_block_hash(block);
|
|
if (ons_parsed_from_block) {
|
|
save_settings(last_processed_height, last_processed_hash, static_cast<int>(DB_VERSION));
|
|
db_transaction.commit = ons_parsed_from_block;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct ons_update_history {
|
|
uint64_t value_last_update_height = static_cast<uint64_t>(-1);
|
|
uint64_t owner_last_update_height = static_cast<uint64_t>(-1);
|
|
uint64_t backup_owner_last_update_height = static_cast<uint64_t>(-1);
|
|
|
|
void update(uint64_t height, cryptonote::tx_extra_oxen_name_system const& ons_extra);
|
|
uint64_t newest_update_height() const;
|
|
};
|
|
|
|
void ons_update_history::update(
|
|
uint64_t height, cryptonote::tx_extra_oxen_name_system const& ons_extra) {
|
|
if (ons_extra.field_is_set(ons::extra_field::encrypted_value))
|
|
value_last_update_height = height;
|
|
|
|
if (ons_extra.field_is_set(ons::extra_field::owner))
|
|
owner_last_update_height = height;
|
|
|
|
if (ons_extra.field_is_set(ons::extra_field::backup_owner))
|
|
backup_owner_last_update_height = height;
|
|
}
|
|
|
|
uint64_t ons_update_history::newest_update_height() const {
|
|
uint64_t result = std::max(
|
|
std::max(value_last_update_height, owner_last_update_height),
|
|
backup_owner_last_update_height);
|
|
return result;
|
|
}
|
|
|
|
struct replay_ons_tx {
|
|
uint64_t height;
|
|
crypto::hash tx_hash;
|
|
cryptonote::tx_extra_oxen_name_system entry;
|
|
};
|
|
|
|
void name_system_db::block_detach(
|
|
cryptonote::Blockchain const& blockchain, uint64_t new_blockchain_height) {
|
|
prune_db(new_blockchain_height);
|
|
}
|
|
|
|
bool name_system_db::save_owner(ons::generic_owner const& owner, int64_t* row_id) {
|
|
bool result = bind_and_run(
|
|
ons_sql_type::save_owner,
|
|
save_owner_sql,
|
|
nullptr,
|
|
blob_view{reinterpret_cast<const char*>(&owner), sizeof(owner)});
|
|
|
|
if (row_id)
|
|
*row_id = sqlite3_last_insert_rowid(db);
|
|
return result;
|
|
}
|
|
|
|
bool name_system_db::save_mapping(
|
|
crypto::hash const& tx_hash,
|
|
cryptonote::tx_extra_oxen_name_system const& src,
|
|
uint64_t height,
|
|
std::optional<uint64_t> expiration,
|
|
int64_t owner_id,
|
|
std::optional<int64_t> backup_owner_id) {
|
|
if (!src.is_buying())
|
|
return false;
|
|
|
|
std::string name_hash = hash_to_base64(src.name_hash);
|
|
auto& statement = save_mapping_sql;
|
|
clear_bindings(statement);
|
|
bind(statement, mapping_record_column::type, db_mapping_type(src.type));
|
|
bind(statement, mapping_record_column::name_hash, name_hash);
|
|
bind(statement, mapping_record_column::encrypted_value, blob_view{src.encrypted_value});
|
|
bind(statement, mapping_record_column::txid, blob_view{tx_hash.data(), tx_hash.size()});
|
|
bind(statement, mapping_record_column::update_height, height);
|
|
bind(statement, mapping_record_column::expiration_height, expiration);
|
|
bind(statement, mapping_record_column::owner_id, owner_id);
|
|
bind(statement, mapping_record_column::backup_owner_id, backup_owner_id);
|
|
|
|
bool result = sql_run_statement(ons_sql_type::save_mapping, statement, nullptr);
|
|
return result;
|
|
}
|
|
|
|
bool name_system_db::save_settings(uint64_t top_height, crypto::hash const& top_hash, int version) {
|
|
auto& statement = save_settings_sql;
|
|
bind(statement, ons_db_setting_column::top_height, top_height);
|
|
bind(statement, ons_db_setting_column::top_hash, blob_view{top_hash.data(), top_hash.size()});
|
|
bind(statement, ons_db_setting_column::version, version);
|
|
bool result = sql_run_statement(ons_sql_type::save_setting, statement, nullptr);
|
|
return result;
|
|
}
|
|
|
|
bool name_system_db::prune_db(uint64_t height) {
|
|
if (!bind_and_run(ons_sql_type::pruning, prune_mappings_sql, nullptr, height))
|
|
return false;
|
|
if (!sql_run_statement(ons_sql_type::pruning, prune_owners_sql, nullptr))
|
|
return false;
|
|
|
|
this->last_processed_height = (height - 1);
|
|
return true;
|
|
}
|
|
|
|
owner_record name_system_db::get_owner_by_key(ons::generic_owner const& owner) {
|
|
owner_record result = {};
|
|
result.loaded = bind_and_run(
|
|
ons_sql_type::get_owner,
|
|
get_owner_by_key_sql,
|
|
&result,
|
|
blob_view{reinterpret_cast<const char*>(&owner), sizeof(owner)});
|
|
return result;
|
|
}
|
|
|
|
owner_record name_system_db::get_owner_by_id(int64_t owner_id) {
|
|
owner_record result = {};
|
|
result.loaded = bind_and_run(ons_sql_type::get_owner, get_owner_by_id_sql, &result, owner_id);
|
|
return result;
|
|
}
|
|
|
|
bool name_system_db::get_wallet_mapping(
|
|
std::string str, uint64_t blockchain_height, cryptonote::address_parse_info& addr_info) {
|
|
std::string name = tools::lowercase_ascii_string(std::move(str));
|
|
std::string b64_hashed_name = ons::name_to_base64_hash(name);
|
|
if (auto record =
|
|
name_system_db::resolve(mapping_type::wallet, b64_hashed_name, blockchain_height)) {
|
|
(*record).decrypt(name, mapping_type::wallet);
|
|
std::optional<cryptonote::address_parse_info> addr = (*record).get_wallet_address_info();
|
|
if (addr) {
|
|
addr_info = *addr;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
mapping_record name_system_db::get_mapping(
|
|
mapping_type type,
|
|
std::string_view name_base64_hash,
|
|
std::optional<uint64_t> blockchain_height) {
|
|
assert(name_base64_hash.size() == 44 && name_base64_hash.back() == '=' &&
|
|
oxenc::is_base64(name_base64_hash));
|
|
mapping_record result = {};
|
|
result.loaded = bind_and_run(
|
|
ons_sql_type::get_mapping,
|
|
get_mapping_sql,
|
|
&result,
|
|
db_mapping_type(type),
|
|
name_base64_hash);
|
|
if (blockchain_height && !result.active(*blockchain_height))
|
|
result.loaded = false;
|
|
return result;
|
|
}
|
|
|
|
std::optional<mapping_value> name_system_db::resolve(
|
|
mapping_type type, std::string_view name_hash_b64, uint64_t blockchain_height) {
|
|
assert(name_hash_b64.size() == 44 && name_hash_b64.back() == '=' &&
|
|
oxenc::is_base64(name_hash_b64));
|
|
std::optional<mapping_value> result;
|
|
bind_all(resolve_sql, db_mapping_type(type), name_hash_b64, blockchain_height);
|
|
if (step(resolve_sql) == SQLITE_ROW) {
|
|
if (auto blob = get<std::optional<blob_view>>(resolve_sql, 0)) {
|
|
auto& r = result.emplace();
|
|
assert(blob->data.size() <= r.buffer.size());
|
|
r.len = blob->data.size();
|
|
r.encrypted = true;
|
|
std::copy(blob->data.begin(), blob->data.end(), r.buffer.begin());
|
|
}
|
|
}
|
|
reset(resolve_sql);
|
|
clear_bindings(resolve_sql);
|
|
return result;
|
|
}
|
|
|
|
std::vector<mapping_record> name_system_db::get_mappings(
|
|
std::vector<mapping_type> const& types,
|
|
std::string_view name_base64_hash,
|
|
std::optional<uint64_t> blockchain_height) {
|
|
assert(name_base64_hash.size() == 44 && name_base64_hash.back() == '=' &&
|
|
oxenc::is_base64(name_base64_hash));
|
|
std::vector<mapping_record> result;
|
|
if (types.empty())
|
|
return result;
|
|
|
|
std::string sql_statement;
|
|
std::vector<std::variant<uint16_t, uint64_t, std::string_view>> bind;
|
|
sql_statement.reserve(
|
|
sql_select_mappings_and_owners_prefix.size() + EXPIRATION.size() + 70 +
|
|
sql_select_mappings_and_owners_suffix.size());
|
|
sql_statement += sql_select_mappings_and_owners_prefix;
|
|
sql_statement += "WHERE name_hash = ?";
|
|
bind.emplace_back(name_base64_hash);
|
|
|
|
// Generate string statement
|
|
if (types.size()) {
|
|
sql_statement += " AND type IN (";
|
|
|
|
for (size_t i = 0; i < types.size(); i++) {
|
|
sql_statement += i > 0 ? ", ?" : "?";
|
|
bind.emplace_back(db_mapping_type(types[i]));
|
|
}
|
|
sql_statement += ")";
|
|
}
|
|
|
|
if (blockchain_height) {
|
|
sql_statement += " AND ";
|
|
sql_statement += EXPIRATION;
|
|
bind.emplace_back(*blockchain_height);
|
|
}
|
|
|
|
sql_statement += sql_select_mappings_and_owners_suffix;
|
|
|
|
// Compile Statement
|
|
sql_compiled_statement statement{*this};
|
|
if (!statement.compile(sql_statement, false /*optimise_for_multiple_usage*/) ||
|
|
!bind_container(statement, bind))
|
|
return result;
|
|
|
|
// Execute
|
|
sql_run_statement(ons_sql_type::get_mappings, statement, &result);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<mapping_record> name_system_db::get_mappings_by_owners(
|
|
std::vector<generic_owner> const& owners, std::optional<uint64_t> blockchain_height) {
|
|
std::string sql_statement;
|
|
std::vector<std::variant<blob_view, uint64_t>> bind;
|
|
// Generate string statement
|
|
{
|
|
constexpr auto SQL_WHERE_OWNER = "WHERE (o1.address IN ("sv;
|
|
constexpr auto SQL_OR_BACKUP_OWNER = ") OR o2.address IN ("sv;
|
|
constexpr auto SQL_SUFFIX = "))"sv;
|
|
|
|
std::string placeholders;
|
|
placeholders.reserve(3 * owners.size());
|
|
for (size_t i = 0; i < owners.size(); i++)
|
|
placeholders += "?, ";
|
|
if (owners.size() > 0)
|
|
placeholders.resize(placeholders.size() - 2);
|
|
|
|
sql_statement.reserve(
|
|
sql_select_mappings_and_owners_prefix.size() + SQL_WHERE_OWNER.size() +
|
|
SQL_OR_BACKUP_OWNER.size() + SQL_SUFFIX.size() + 2 * placeholders.size() + 5 +
|
|
EXPIRATION.size() + sql_select_mappings_and_owners_suffix.size());
|
|
sql_statement += sql_select_mappings_and_owners_prefix;
|
|
sql_statement += SQL_WHERE_OWNER;
|
|
sql_statement += placeholders;
|
|
sql_statement += SQL_OR_BACKUP_OWNER;
|
|
sql_statement += placeholders;
|
|
sql_statement += SQL_SUFFIX;
|
|
|
|
for (int i : {0, 1})
|
|
for (auto const& owner : owners)
|
|
bind.emplace_back(blob_view{reinterpret_cast<const char*>(&owner), sizeof(owner)});
|
|
}
|
|
|
|
if (blockchain_height) {
|
|
sql_statement += " AND ";
|
|
sql_statement += EXPIRATION;
|
|
bind.emplace_back(*blockchain_height);
|
|
}
|
|
|
|
sql_statement += sql_select_mappings_and_owners_suffix;
|
|
|
|
// Compile Statement
|
|
std::vector<mapping_record> result;
|
|
sql_compiled_statement statement{*this};
|
|
if (!statement.compile(sql_statement, false /*optimise_for_multiple_usage*/) ||
|
|
!bind_container(statement, bind))
|
|
return result;
|
|
|
|
// Execute
|
|
sql_run_statement(ons_sql_type::get_mappings_by_owners, statement, &result);
|
|
return result;
|
|
}
|
|
|
|
std::vector<mapping_record> name_system_db::get_mappings_by_owner(
|
|
generic_owner const& owner, std::optional<uint64_t> blockchain_height) {
|
|
std::vector<mapping_record> result = {};
|
|
blob_view ownerblob{reinterpret_cast<const char*>(&owner), sizeof(owner)};
|
|
bind_and_run(
|
|
ons_sql_type::get_mappings_by_owner,
|
|
get_mappings_by_owner_sql,
|
|
&result,
|
|
ownerblob,
|
|
ownerblob);
|
|
if (blockchain_height) {
|
|
auto end = std::remove_if(
|
|
result.begin(), result.end(), [height = *blockchain_height](auto& r) {
|
|
return !r.active(height);
|
|
});
|
|
result.erase(end, result.end());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::map<mapping_type, int> name_system_db::get_mapping_counts(uint64_t blockchain_height) {
|
|
std::map<mapping_type, int> result;
|
|
bind_and_run(
|
|
ons_sql_type::get_mapping_counts, get_mapping_counts_sql, &result, blockchain_height);
|
|
return result;
|
|
}
|
|
|
|
settings_record name_system_db::get_settings() {
|
|
settings_record result = {};
|
|
result.loaded = sql_run_statement(ons_sql_type::get_setting, get_settings_sql, &result);
|
|
return result;
|
|
}
|
|
|
|
} // namespace ons
|