oxen-core/src/cryptonote_core/oxen_name_system.cpp

2276 lines
86 KiB
C++

#include <bitset>
#include <variant>
#include "common/hex.h"
#include "oxen_name_system.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_core/blockchain.h"
#include "oxen_economy.h"
#include <lokimq/hex.h>
#include <lokimq/base32z.h>
#include <lokimq/base64.h>
#include <sqlite3.h>
extern "C"
{
#include <sodium/crypto_generichash.h>
#include <sodium/crypto_generichash_blake2b.h>
#include <sodium/crypto_pwhash.h>
#include <sodium/crypto_secretbox.h>
#include <sodium/crypto_aead_xchacha20poly1305.h>
#include <sodium/crypto_sign.h>
#include <sodium/randombytes.h>
}
#undef OXEN_DEFAULT_LOG_CATEGORY
#define OXEN_DEFAULT_LOG_CATEGORY "lns"
namespace lns
{
enum struct lns_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_owner,
get_setting,
get_sentinel_end,
internal_cmd,
};
enum struct lns_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>> lns::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 lns::mapping_value::to_readable_value(cryptonote::network_type nettype, lns::mapping_type type) const
{
std::string result;
if (is_lokinet_type(type))
{
result = lokimq::to_base32z(to_view()) + ".oxen";
}
else if (type == lns::mapping_type::wallet)
{
cryptonote::address_parse_info addr_info = {};
if (len == sizeof(addr_info))
{
std::memcpy(&addr_info, buffer.data(), len);
result = cryptonote::get_account_address_as_str(nettype, addr_info.is_subaddress, addr_info.address);
}
else
result = "(error unknown wallet address)";
}
else
{
result = lokimq::to_hex(to_view());
}
return result;
}
namespace {
std::string lns_extra_string(cryptonote::network_type nettype, cryptonote::tx_extra_oxen_name_system const &data)
{
std::stringstream stream;
stream << "LNS Extra={";
if (data.is_buying())
{
stream << "owner=" << data.owner.to_string(nettype);
stream << ", backup_owner=" << (data.backup_owner ? data.backup_owner.to_string(nettype) : "(none)");
}
else if (data.is_renewing())
stream << "renewal";
else
stream << "signature=" << tools::type_to_hex(data.signature.data);
stream << ", type=" << data.type << ", name_hash=" << data.name_hash << "}";
return stream.str();
}
/// 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) <= 32), 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) > 32), 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)...} {}
};
// 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 lns::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 lns::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 : {lns::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 (!lns::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_PRINT_L0("Unexpected blob size=" << blob.data.size() << ", in LNS DB does not match expected 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())
{
MERROR("Unexpected encrypted value blob with size=" << value.size() << ", in LNS db larger than the available 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, sizeof(result.txid)))
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(lns_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: MERROR("Unhandled lns type enum with value: " << (int)type << ", in: " << __func__); break;
case lns_sql_type::internal_cmd: break;
case lns_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 lns_sql_type::get_setting:
{
auto *entry = reinterpret_cast<settings_record *>(context);
get(statement, lns_db_setting_column::top_height, entry->top_height);
if (!sql_copy_blob(statement, lns_db_setting_column::top_hash, entry->top_hash.data, sizeof(entry->top_hash.data)))
return false;
get(statement, lns_db_setting_column::version, entry->version);
data_loaded = true;
}
break;
case lns_sql_type::get_mappings_by_owners: /* FALLTHRU */
case lns_sql_type::get_mappings_by_owner: /* FALLTHRU */
case lns_sql_type::get_mappings: /* FALLTHRU */
case lns_sql_type::get_mapping:
{
if (mapping_record tmp_entry = sql_get_mapping_from_statement(statement))
{
data_loaded = true;
if (type == lns_sql_type::get_mapping)
{
auto *entry = reinterpret_cast<mapping_record *>(context);
*entry = std::move(tmp_entry);
}
else
{
auto *records = reinterpret_cast<std::vector<mapping_record> *>(context);
records->emplace_back(std::move(tmp_entry));
}
}
}
break;
}
}
break;
case SQLITE_BUSY: break;
case SQLITE_DONE:
{
infinite_loop = false;
result = (type > lns_sql_type::get_sentinel_start && type < lns_sql_type::get_sentinel_end) ? data_loaded : true;
break;
}
default:
{
LOG_PRINT_L1("Failed to execute statement: " << sqlite3_sql(statement.statement) <<", reason: " << 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(lns_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
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) {
MERROR("Can not compile SQL statement:\n" << query << "\nReason: " << 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)
{
MERROR("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)
{
MERROR("Failed to open LNS db at: " << file_path << ", reason: " << 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)
{
MERROR("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)
{
MERROR("Failed to set synchronous mode to NORMAL: " << sqlite3_errstr(exec));
return nullptr;
}
return result;
}
std::vector<mapping_type> all_mapping_types(uint8_t hf_version) {
std::vector<mapping_type> result;
result.reserve(2);
if (hf_version >= cryptonote::network_version_15_lns)
result.push_back(mapping_type::session);
if (hf_version >= cryptonote::network_version_16_pulse)
result.push_back(mapping_type::lokinet);
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::TESTNET && type != mapping_type::lokinet_10years;
if (type == mapping_type::lokinet) result = BLOCKS_EXPECTED_IN_DAYS(1 * REGISTRATION_YEAR_DAYS);
else if (type == mapping_type::lokinet_2years) result = BLOCKS_EXPECTED_IN_DAYS(2 * REGISTRATION_YEAR_DAYS);
else if (type == mapping_type::lokinet_5years) result = BLOCKS_EXPECTED_IN_DAYS(5 * REGISTRATION_YEAR_DAYS);
else if (type == mapping_type::lokinet_10years) result = BLOCKS_EXPECTED_IN_DAYS(10 * REGISTRATION_YEAR_DAYS);
assert(result);
if (testnet_short)
*result /= REGISTRATION_YEAR_DAYS;
else if (nettype == cryptonote::FAKECHAIN) // For fakenet testing we shorten 1/2/5/10 years to 2/4/10/20 blocks
*result /= (BLOCKS_EXPECTED_IN_DAYS(REGISTRATION_YEAR_DAYS) / 2);
}
return result;
}
static void append_owner(std::string& buffer, const lns::generic_owner* owner)
{
if (owner) {
buffer += static_cast<char>(owner->type);
buffer += owner->type == lns::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, lns::generic_owner const *owner, lns::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)
{
MERROR("Unexpected value len=" << value.size() << " greater than the expected capacity=" << 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;
}
lns::generic_signature make_ed25519_signature(crypto::hash const &hash, crypto::ed25519_secret_key const &skey)
{
lns::generic_signature result = {};
result.type = lns::generic_owner_sig_type::ed25519;
crypto_sign_detached(result.ed25519.data, NULL, reinterpret_cast<unsigned char const *>(hash.data), sizeof(hash), skey.data);
return result;
}
lns::generic_owner make_monero_owner(cryptonote::account_public_address const &owner, bool is_subaddress)
{
lns::generic_owner result = {};
result.type = lns::generic_owner_sig_type::monero;
result.wallet.address = owner;
result.wallet.is_subaddress = is_subaddress;
return result;
}
lns::generic_owner make_ed25519_owner(crypto::ed25519_public_key const &pkey)
{
lns::generic_owner result = {};
result.type = lns::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 = lns::make_monero_owner(parsed_addr.address, parsed_addr.is_subaddress);
}
else if (owner.size() == 2*sizeof(ed_owner.data) && lokimq::is_hex(owner))
{
lokimq::from_hex(owner.begin(), owner.end(), ed_owner.data);
result = lns::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, T&&... args) {
if (condition && reason)
{
std::ostringstream os;
(os << ... << std::forward<T>(args));
*reason = os.str();
}
return condition;
}
bool validate_lns_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 = lns::SESSION_DISPLAY_NAME_MAX;
else if (type == mapping_type::wallet) max_name_len = lns::WALLET_NAME_MAX;
else
{
if (reason)
{
std::stringstream err_stream;
err_stream << "LNS type=" << mapping_type_str(type) << ", specifies unhandled mapping type in name validation";
*reason = err_stream.str();
}
return false;
}
// NOTE: Validate name length
name = tools::lowercase_ascii_string(name);
if (check_condition((name.empty() || name.size() > max_name_len), reason, "LNS type=", type, ", specifies mapping from name->value where the name's length=", name.size(), " is 0 or exceeds the maximum length=", max_name_len, ", given name=", 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>'.oxen' must be alphanumeric followed by the suffix '.oxen'
// It's *approximately* this regex, but there are some extra restrictions below
// ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.oxen$
// Reserved names:
// - localhost.oxen has special meaning within lokinet (it is always a CNAME to the local
// address)
// - oxen.oxen and snode.oxen are prohibited in case someone added .oxen or .snode as search
// domains (in which case the user looking up "foo.oxen" would try end up trying to resolve
// "foo.oxen.oxen").
for (auto& reserved : {"localhost.oxen"sv, "oxen.oxen"sv, "snode.oxen"sv})
if (check_condition(name == reserved, reason, "LNS type=", type, ", specifies mapping from name->value using protocol reserved name=", name))
return false;
auto constexpr SHORTEST_DOMAIN = "a.oxen"sv;
if (check_condition(name.size() < SHORTEST_DOMAIN.size(), reason, "LNS type=", type, ", specifies mapping from name->value where the name is shorter than the shortest possible name=", SHORTEST_DOMAIN, ", given name=", name))
return false;
// Must end with .oxen
auto constexpr SUFFIX = ".oxen"sv;
if (check_condition(!tools::ends_with(name_view, SUFFIX), reason, "LNS type=", type, ", specifies mapping from name->value where the name does not end with the domain .oxen, name=", 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, "LNS type=", type, ", specifies reserved name `?\?--*.oxen': ", name))
return false;
// Must start with alphanumeric
if (check_condition(!char_is_alphanum(name_view.front()), reason, "LNS type=", type, ", specifies mapping from name->value where the name does not start with an alphanumeric character, name=", name))
return false;
name_view.remove_prefix(1);
if (!name_view.empty()) {
// Character preceding .oxen must be alphanumeric
if (check_condition(!char_is_alphanum(name_view.back()), reason, "LNS type=", type ,", specifies mapping from name->value where the character preceding the .oxen is not alphanumeric, char=", name_view.back(), ", name=", 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, "LNS type=", type, ", specifies mapping from name->value where the domain name contains more than the permitted alphanumeric or hyphen characters, name=", name))
return false;
}
else if (type == mapping_type::session)
{
// SESSION
// 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, "LNS type=", type, ", specifies mapping from name->value where the name does not start with an alphanumeric or underscore character, name=", 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, "LNS type=", type, ", specifies mapping from name->value where the last character is a hyphen '-' which is disallowed, name=", 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, "LNS type=", type, ", specifies mapping from name->value where the name contains more than the permitted alphanumeric, underscore or hyphen characters, name=", name))
return false;
}
else
{
MERROR("Wallet names not yet implemented");
return false;
}
return true;
}
static bool check_lengths(mapping_type type, std::string_view value, size_t max, bool binary_val, std::string *reason)
{
bool result = (value.size() == max);
if (!result)
{
if (reason)
{
std::stringstream err_stream;
err_stream << "LNS type=" << type << ", specifies mapping from name_hash->encrypted_value where the value's length=" << value.size() << ", does not equal the required length=" << max << ", given value=";
if (binary_val) err_stream << lokimq::to_hex(value);
else err_stream << value;
*reason = err_stream.str();
}
}
return result;
}
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
std::stringstream err_stream;
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())
{
err_stream << "The value=" << value;
err_stream << ", mapping into the wallet address, specifies a wallet address of 0 length";
}
else
{
err_stream << "Could not convert the wallet address string, check it is correct, value=" << value;
}
*reason = err_stream.str();
}
return false;
}
// Validate blob contents and generate the binary form if possible
if (blob)
{
blob->len = sizeof(addr_info);
std::memcpy(blob->buffer.data(), &addr_info, blob->len);
}
}
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, ".oxen") || !lokimq::is_base32z(value.substr(0, 52)) || !(value[51] == 'y' || value[51] == 'o'),
reason, "'", value, "' is not a valid lokinet address"))
return false;
if (blob)
{
blob->len = sizeof(crypto::ed25519_public_key);
lokimq::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=", value, " is not the required ", 2*SESSION_PUBLIC_KEY_BINARY_LENGTH, "-character hex string session public key, length=", value.size()))
return false;
if (check_condition(!lokimq::is_hex(value), reason, ", specifies name -> value mapping where the value is not a hex string given 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, "LNS 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());
lokimq::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 = {};
std::stringstream err_stream;
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 += WALLET_ACCOUNT_BINARY_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)
{
err_stream << "Unhandled type passed into " << __func__;
*reason = err_stream.str();
}
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;
}
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 lokimq::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 && lokimq::is_hex(input))
return name_hash_bytes_to_base64(lokimq::from_hex(input));
if (input.size() >= NAME_HASH_SIZE_B64_MIN && input.size() <= NAME_HASH_SIZE_B64_MAX && lokimq::is_base64(input)) {
std::string tmp = lokimq::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_lns_signature(crypto::hash const &hash, lns::generic_signature const &signature, lns::generic_owner const &owner)
{
if (!owner || !signature) return false;
if (owner.type != signature.type) return false;
if (signature.type == lns::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, reinterpret_cast<unsigned char const *>(hash.data), sizeof(hash.data), owner.ed25519.data) == 0);
}
}
static bool validate_against_previous_mapping(lns::name_system_db &lns_db, uint64_t blockchain_height, cryptonote::transaction const &tx, cryptonote::tx_extra_oxen_name_system const &lns_extra, std::string *reason)
{
std::stringstream err_stream;
OXEN_DEFER { if (reason && reason->empty()) *reason = err_stream.str(); };
crypto::hash expected_prev_txid = crypto::null_hash;
std::string name_hash = hash_to_base64(lns_extra.name_hash);
lns::mapping_record mapping = lns_db.get_mapping(lns_extra.type, name_hash);
if (lns_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, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " update requested but mapping does not exist."))
return false;
if (check_condition(!mapping.active(blockchain_height), reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " TX requested to update mapping that has already expired"))
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(lns_extra.field_is_set(lns::extra_field::encrypted_value) && lns_extra.encrypted_value == mapping.encrypted_value.to_view(), reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), SPECIFYING_SAME_VALUE_ERR, "value"))
return false;
if (check_condition(lns_extra.field_is_set(lns::extra_field::owner) && lns_extra.owner == mapping.owner, reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), SPECIFYING_SAME_VALUE_ERR, "owner"))
return false;
if (check_condition(lns_extra.field_is_set(lns::extra_field::backup_owner) && lns_extra.backup_owner == mapping.backup_owner, reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), SPECIFYING_SAME_VALUE_ERR, "backup_owner"))
return false;
// Validate signature
auto data = tx_extra_signature(
lns_extra.encrypted_value,
lns_extra.field_is_set(lns::extra_field::owner) ? &lns_extra.owner : nullptr,
lns_extra.field_is_set(lns::extra_field::backup_owner) ? &lns_extra.backup_owner : nullptr,
expected_prev_txid);
if (check_condition(data.empty(), reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " unexpectedly failed to generate signature, please inform the Loki developers"))
return false;
crypto::hash hash;
crypto_generichash(reinterpret_cast<unsigned char*>(hash.data), sizeof(hash), reinterpret_cast<const unsigned char*>(data.data()), data.size(), nullptr /*key*/, 0 /*key_len*/);
if (check_condition(!verify_lns_signature(hash, lns_extra.signature, mapping.owner) &&
!verify_lns_signature(hash, lns_extra.signature, mapping.backup_owner), reason,
tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " failed to verify signature for LNS update, current owner=", mapping.owner.to_string(lns_db.network_type()), ", backup owner=", mapping.backup_owner.to_string(lns_db.network_type())))
return false;
}
else if (lns_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 LNS name that is already registered: name_hash=", mapping.name_hash, ", type=", mapping.type,
"; TX: ", tx, "; ", lns_extra_string(lns_db.network_type(), lns_extra)))
return false;
}
else if (lns_extra.is_renewing())
{
// We allow anyone to renew a name, but it has to exist and be currently active
if (check_condition(!mapping, reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " renewal requested but mapping does not exist."))
return false;
if (check_condition(!mapping.active(blockchain_height), reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " TX requested to renew mapping that has already expired"))
return false;
expected_prev_txid = mapping.txid;
}
else
{
check_condition(true, reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " is not a valid buy, update, or renew LNS tx");
return false;
}
if (check_condition(lns_extra.prev_txid != expected_prev_txid, reason, tx, ", ", lns_extra_string(lns_db.network_type(), lns_extra), " specified prior txid=", lns_extra.prev_txid, ", but LNS DB reports=", expected_prev_txid, ", possible competing TX was submitted and accepted before this TX was processed"))
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_lns_tx(uint8_t hf_version, uint64_t blockchain_height, cryptonote::transaction const &tx, cryptonote::tx_extra_oxen_name_system &lns_extra, std::string *reason)
{
// -----------------------------------------------------------------------------------------------
// Pull out LNS Extra from TX
// -----------------------------------------------------------------------------------------------
{
if (check_condition(tx.type != cryptonote::txtype::oxen_name_system, reason, tx, ", uses wrong tx type, expected=", cryptonote::txtype::oxen_name_system))
return false;
if (check_condition(!cryptonote::get_field_from_tx_extra(tx.extra, lns_extra), reason, tx, ", didn't have oxen name service in the tx_extra"))
return false;
}
// -----------------------------------------------------------------------------------------------
// Check TX LNS 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(!lns_extra.field_is_set(lns::extra_field::encrypted_value) && lns_extra.encrypted_value.size(), reason, tx, ", ", lns_extra_string(nettype, lns_extra), VALUE_SPECIFIED_BUT_NOT_REQUESTED, "encrypted_value"))
return false;
if (check_condition(!lns_extra.field_is_set(lns::extra_field::owner) && lns_extra.owner, reason, tx, ", ", lns_extra_string(nettype, lns_extra), VALUE_SPECIFIED_BUT_NOT_REQUESTED, "owner"))
return false;
if (check_condition(!lns_extra.field_is_set(lns::extra_field::backup_owner) && lns_extra.backup_owner, reason, tx, ", ", lns_extra_string(nettype, lns_extra), VALUE_SPECIFIED_BUT_NOT_REQUESTED, "backup_owner"))
return false;
if (check_condition(!lns_extra.field_is_set(lns::extra_field::signature) && lns_extra.signature, reason, tx, ", ", lns_extra_string(nettype, lns_extra), VALUE_SPECIFIED_BUT_NOT_REQUESTED, "signature"))
return false;
}
// -----------------------------------------------------------------------------------------------
// Simple LNS Extra Validation
// -----------------------------------------------------------------------------------------------
{
if (check_condition(lns_extra.version != 0, reason, tx, ", ", lns_extra_string(nettype, lns_extra), " unexpected version=", std::to_string(lns_extra.version), ", expected=0"))
return false;
if (check_condition(!lns::mapping_type_allowed(hf_version, lns_extra.type), reason, tx, ", ", lns_extra_string(nettype, lns_extra), " specifying type=", lns_extra.type, " that is disallowed in hardfork ", hf_version))
return false;
// -----------------------------------------------------------------------------------------------
// Serialized Values Check
// -----------------------------------------------------------------------------------------------
if (check_condition(!lns_extra.is_buying() && !lns_extra.is_updating() && !lns_extra.is_renewing(), reason, tx, ", ", lns_extra_string(nettype, lns_extra), " TX extra does not specify valid combination of bits for serialized fields=", std::bitset<sizeof(lns_extra.fields) * 8>(static_cast<size_t>(lns_extra.fields)).to_string()))
return false;
if (check_condition(lns_extra.field_is_set(lns::extra_field::owner) &&
lns_extra.field_is_set(lns::extra_field::backup_owner) &&
lns_extra.owner == lns_extra.backup_owner,
reason, tx, ", ", lns_extra_string(nettype, lns_extra), " specifying owner the same as the backup owner=", lns_extra.backup_owner.to_string(nettype)))
{
return false;
}
}
// -----------------------------------------------------------------------------------------------
// LNS Field(s) Validation
// -----------------------------------------------------------------------------------------------
{
if (check_condition((lns_extra.name_hash == null_name_hash || lns_extra.name_hash == crypto::null_hash), reason, tx, ", ", lns_extra_string(nettype, lns_extra), " specified the null name hash"))
return false;
if (lns_extra.field_is_set(lns::extra_field::encrypted_value))
{
if (!mapping_value::validate_encrypted(lns_extra.type, lns_extra.encrypted_value, nullptr, reason))
return false;
}
if (!validate_against_previous_mapping(*this, blockchain_height, tx, lns_extra, reason))
return false;
}
// -----------------------------------------------------------------------------------------------
// Burn Validation
// -----------------------------------------------------------------------------------------------
{
uint64_t burn = cryptonote::get_burned_amount_from_tx_extra(tx.extra);
uint64_t const burn_required = (lns_extra.is_buying() || lns_extra.is_renewing()) ? burn_needed(hf_version, lns_extra.type) : 0;
if (burn != burn_required)
{
char const *over_or_under = burn > burn_required ? "too much " : "insufficient ";
if (check_condition(true, reason, tx, ", ", lns_extra_string(nettype, lns_extra), " burned ", over_or_under, "oxen=", burn, ", require=", burn_required))
return false;
}
}
return true;
}
bool validate_mapping_type(std::string_view mapping_type_str, uint8_t hf_version, lns_tx_type txtype, lns::mapping_type *mapping_type, std::string *reason)
{
std::string mapping = tools::lowercase_ascii_string(mapping_type_str);
std::optional<lns::mapping_type> mapping_type_;
if (txtype != lns_tx_type::renew && tools::string_iequal(mapping, "session"))
mapping_type_ = lns::mapping_type::session;
else if (hf_version >= cryptonote::network_version_16_pulse)
{
if (tools::string_iequal(mapping, "lokinet"))
mapping_type_ = lns::mapping_type::lokinet;
else if (txtype == lns_tx_type::buy || txtype == lns_tx_type::renew)
{
if (tools::string_iequal_any(mapping, "lokinet_1y", "lokinet_1years")) // Can also specify "lokinet"
mapping_type_ = lns::mapping_type::lokinet;
else if (tools::string_iequal_any(mapping, "lokinet_2y", "lokinet_2years"))
mapping_type_ = lns::mapping_type::lokinet_2years;
else if (tools::string_iequal_any(mapping, "lokinet_5y", "lokinet_5years"))
mapping_type_ = lns::mapping_type::lokinet_5years;
else if (tools::string_iequal_any(mapping, "lokinet_10y", "lokinet_10years"))
mapping_type_ = lns::mapping_type::lokinet_10years;
}
}
if (!mapping_type_)
{
if (reason) *reason = "Unsupported LNS type \"" + std::string{mapping_type_str} + "\"; supported " + (
txtype == lns_tx_type::update ? "update types are: session, lokinet" :
txtype == lns_tx_type::renew ? "renew types are: lokinet_1y, lokinet_2y, lokinet_5y, lokinet_10y" :
txtype == lns_tx_type::buy ? "buy types are session, lokinet_1y, lokinet_2y, lokinet_5y, lokinet_10y"
: "lookup types are session, lokinet");
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(reinterpret_cast<unsigned char *>(result.data),
sizeof(result),
reinterpret_cast<const unsigned char *>(name.data()),
static_cast<unsigned long long>(name.size()),
key ? reinterpret_cast<const unsigned char*>(key->data) : nullptr,
key ? sizeof(key->data) : 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);
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())
{
MERROR("Encrypted value pre-allocated buffer too small=" << buffer.size() << ", required=" << 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: dec_length = WALLET_ACCOUNT_BINARY_LENGTH; break;
default: MERROR("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)
{
MERROR("Encrypted value size is invalid=" << len << ", expected=" << 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;
}
namespace {
bool build_default_tables(name_system_db& lns_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");
CREATE INDEX IF NOT EXISTS "backup_owner_id_index" ON mappings("backup_owner_index");
)";
char *table_err_msg = nullptr;
int table_created = sqlite3_exec(lns_db.db, BUILD_TABLE_SQL.c_str(), nullptr /*callback*/, nullptr /*callback context*/, &table_err_msg);
if (table_created != SQLITE_OK)
{
MERROR("Can not generate SQL table for LNS: " << (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{lns_db};
mappings_info.compile(R"(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(lns_db.db,
R"(ALTER TABLE "mappings" ADD COLUMN "update_height" INTEGER NOT NULL DEFAULT "register_height")",
nullptr /*callback*/, nullptr /*callback ctx*/, nullptr /*errstr*/);
LOG_PRINT_L1("Migrating LNS 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_id_index" ON mappings("backup_owner_index");
COMMIT TRANSACTION;
)";
int migrated = sqlite3_exec(lns_db.db, migrate.c_str(), nullptr /*callback*/, nullptr /*callback context*/, &table_err_msg);
if (migrated != SQLITE_OK)
{
MERROR("Can not migrate SQL mappings table for LNS: " << (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 : {
R"(ALTER TABLE "settings" ADD COLUMN "pruned_height" INTEGER NOT NULL DEFAULT 0)",
}) {
sqlite3_exec(lns_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 = R"( GROUP BY "name_hash", "type")";
struct scoped_db_transaction
{
scoped_db_transaction(name_system_db &lns_db);
~scoped_db_transaction();
operator bool() const { return initialised; }
name_system_db &lns_db;
bool commit = false; // If true, on destruction- END the transaction otherwise ROLLBACK all SQLite events prior for the lns_db
bool initialised = false;
};
scoped_db_transaction::scoped_db_transaction(name_system_db &lns_db)
: lns_db(lns_db)
{
if (lns_db.transaction_begun)
{
MERROR("Failed to begin transaction, transaction exists previously that was not closed properly");
return;
}
char *sql_err = nullptr;
if (sqlite3_exec(lns_db.db, "BEGIN;", nullptr, nullptr, &sql_err) != SQLITE_OK)
{
MERROR("Failed to begin transaction " << ", reason=" << (sql_err ? sql_err : "??"));
sqlite3_free(sql_err);
return;
}
initialised = true;
lns_db.transaction_begun = true;
}
scoped_db_transaction::~scoped_db_transaction()
{
if (!initialised) return;
if (!lns_db.transaction_begun)
{
MERROR("Trying to apply non-existent transaction (no prior history of a db transaction beginning) to the LNS DB");
return;
}
char *sql_err = nullptr;
if (sqlite3_exec(lns_db.db, commit ? "END;" : "ROLLBACK;", NULL, NULL, &sql_err) != SQLITE_OK)
{
MERROR("Failed to " << (commit ? "end " : "rollback ") << " transaction to LNS DB, reason=" << (sql_err ? sql_err : "??"));
sqlite3_free(sql_err);
return;
}
lns_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 = R"( ("expiration_height" IS NULL OR "expiration_height" >= ?) )"sv;
} // anon. 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
+ R"(WHERE ? IN ("o1"."address", "o2"."address"))"
+ sql_select_mappings_and_owners_suffix;
std::string const get_mapping_str = sql_select_mappings_and_owners_prefix
+ R"(WHERE "type" = ? AND "name_hash" = ?)"
+ sql_select_mappings_and_owners_suffix;
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 = R"(SELECT * FROM "settings" WHERE "id" = 1)"sv;
constexpr auto GET_OWNER_BY_ID_STR = R"(SELECT * FROM "owner" WHERE "id" = ?)"sv;
constexpr auto GET_OWNER_BY_KEY_STR = R"(SELECT * FROM "owner" WHERE "address" = ?)"sv;
constexpr auto PRUNE_MAPPINGS_STR = R"(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 = R"(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 = R"(INSERT INTO "owner" ("address") VALUES (?))"sv;
constexpr auto SAVE_SETTINGS_STR = R"(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)
{
MERROR("Migration required, blockchain can not be nullptr");
return false;
}
if (blockchain->get_db().is_read_only())
{
MERROR("DB is opened in read-only mode, unable to migrate LNS 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(lns_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 = R"(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(lns_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(R"(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) ||
!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::FAKECHAIN);
return nettype == cryptonote::FAKECHAIN;
}
uint64_t lns_height = 0;
crypto::hash lns_hash = blockchain->get_tail_id(lns_height);
// Try support out of date LNS 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 lns_blk = {};
bool orphan = false;
if (blockchain->get_block_by_hash(settings.top_hash, lns_blk, &orphan))
{
bool lns_height_matches = settings.top_height == cryptonote::get_block_height(lns_blk);
if (lns_height_matches && !orphan)
{
lns_height = settings.top_height;
lns_hash = settings.top_hash;
}
}
if (settings.top_height == lns_height && settings.top_hash == lns_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 lns db and blockchain are out of sync.
// This likely means something external changed the lmdb and/or the lns.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[] = R"(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(lns::name_system_db &lns_db, crypto::hash const &tx_hash, cryptonote::tx_extra_oxen_name_system const &entry, lns::generic_owner const &key)
{
int64_t result = 0;
if (owner_record owner = lns_db.get_owner_by_key(key)) result = owner.id;
if (result == 0)
{
if (!lns_db.save_owner(key, &result))
{
LOG_PRINT_L1("Failed to save LNS owner to DB tx: " << tx_hash << ", type: " << entry.type << ", name_hash: " << entry.name_hash << ", owner: " << entry.owner.to_string(lns_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& lns_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, sizeof(tx_hash)});
bind.emplace_back(height);
constexpr auto suffix = R"(
FROM "mappings" WHERE "type" = ? AND "name_hash" = ? ORDER BY "update_height" DESC LIMIT 1)"sv;
if (entry.is_renewing())
{
sql += R"(, "expiration_height" + ?, "owner_id", "backup_owner_id", "encrypted_value")";
bind.emplace_back(expiry_blocks(lns_db.network_type(), entry.type).value_or(0));
}
else
{
// Updating
sql += R"(, "expiration_height")";
if (entry.field_is_set(lns::extra_field::owner))
{
auto opt_id = add_or_get_owner_id(lns_db, tx_hash, entry, entry.owner);
if (!opt_id)
{
MERROR("Failed to add or get owner with key=" << entry.owner.to_string(lns_db.network_type()));
assert(opt_id);
return {};
}
sql += ", ?";
bind.emplace_back(*opt_id);
}
else
sql += R"(, "owner_id")";
if (entry.field_is_set(lns::extra_field::backup_owner))
{
auto opt_id = add_or_get_owner_id(lns_db, tx_hash, entry, entry.backup_owner);
if (!opt_id)
{
MERROR("Failed to add or get backup owner with key=" << entry.backup_owner.to_string(lns_db.network_type()));
assert(opt_id);
return {};
}
sql += ", ?";
bind.emplace_back(*opt_id);
}
else
sql += R"(, "backup_owner_id")";
if (entry.field_is_set(lns::extra_field::encrypted_value))
{
sql += ", ?";
bind.emplace_back(blob_view{entry.encrypted_value});
}
else
sql += R"(, "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_lns_entry(lns::name_system_db &lns_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(lns_db, tx_hash, entry, entry.owner);
if (!owner_id)
{
MERROR("Failed to add or get owner with key=" << entry.owner.to_string(lns_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(lns_db, tx_hash, entry, entry.backup_owner);
if (!backup_owner_id)
{
MERROR("Failed to add or get backup owner with key=" << entry.backup_owner.to_string(lns_db.network_type()));
assert(backup_owner_id);
return false;
}
}
auto expiry = expiry_blocks(lns_db.network_type(), entry.type);
if (expiry) *expiry += height;
if (!lns_db.save_mapping(tx_hash, entry, height, expiry, *owner_id, backup_owner_id))
{
LOG_PRINT_L1("Failed to save LNS entry to DB tx: " << tx_hash << ", type: " << entry.type << ", name_hash: " << entry.name_hash << ", owner: " << entry.owner.to_string(lns_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(lns_db, height, entry, tx_hash);
if (sql.empty())
return false; // already MERROR'd
// Compile sql statement
sql_compiled_statement statement{lns_db};
if (!statement.compile(sql, false /*optimise_for_multiple_usage*/))
{
MERROR("Failed to compile SQL statement for updating LNS record=" << sql);
return false;
}
// Bind statement parameters
bind_container(statement, bind);
if (!sql_run_statement(lns_sql_type::save_mapping, statement, nullptr))
return false;
}
return true;
}
} // anon 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 lns_parsed_from_block = false;
if (block.major_version >= cryptonote::network_version_15_lns)
{
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_lns_tx(block.major_version, height, tx, entry, &fail_reason))
{
LOG_PRINT_L0("LNS TX: Failed to validate for tx=" << get_transaction_hash(tx) << ". This should have failed validation earlier reason=" << 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_lns_entry(*this, height, entry, tx_hash))
return false;
lns_parsed_from_block = true;
}
}
last_processed_height = height;
last_processed_hash = cryptonote::get_block_hash(block);
if (lns_parsed_from_block)
{
save_settings(last_processed_height, last_processed_hash, static_cast<int>(DB_VERSION));
db_transaction.commit = lns_parsed_from_block;
}
return true;
}
struct lns_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 &lns_extra);
uint64_t newest_update_height() const;
};
void lns_update_history::update(uint64_t height, cryptonote::tx_extra_oxen_name_system const &lns_extra)
{
if (lns_extra.field_is_set(lns::extra_field::encrypted_value))
value_last_update_height = height;
if (lns_extra.field_is_set(lns::extra_field::owner))
owner_last_update_height = height;
if (lns_extra.field_is_set(lns::extra_field::backup_owner))
backup_owner_last_update_height = height;
}
uint64_t lns_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_lns_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(lns::generic_owner const &owner, int64_t *row_id)
{
bool result = bind_and_run(lns_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, sizeof(tx_hash)});
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(lns_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, lns_db_setting_column::top_height, top_height);
bind(statement, lns_db_setting_column::top_hash, blob_view{top_hash.data, sizeof(top_hash)});
bind(statement, lns_db_setting_column::version, version);
bool result = sql_run_statement(lns_sql_type::save_setting, statement, nullptr);
return result;
}
bool name_system_db::prune_db(uint64_t height)
{
if (!bind_and_run(lns_sql_type::pruning, prune_mappings_sql, nullptr, height)) return false;
if (!sql_run_statement(lns_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(lns::generic_owner const &owner)
{
owner_record result = {};
result.loaded = bind_and_run(lns_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(lns_sql_type::get_owner, get_owner_by_id_sql, &result,
owner_id);
return result;
}
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() == '=' && lokimq::is_base64(name_base64_hash));
mapping_record result = {};
result.loaded = bind_and_run(lns_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() == '=' && lokimq::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() == '=' && lokimq::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 += R"(WHERE "name_hash" = ?)";
bind.emplace_back(name_base64_hash);
// Generate string statement
if (types.size())
{
sql_statement += R"( 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(lns_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 = R"(WHERE ("o1"."address" IN ()"sv;
constexpr auto SQL_OR_BACKUP_OWNER = R"() 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(lns_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(lns_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;
}
settings_record name_system_db::get_settings()
{
settings_record result = {};
result.loaded = sql_run_statement(lns_sql_type::get_setting, get_settings_sql, &result);
return result;
}
} // namespace lns