oxen-core/src/cryptonote_core/loki_name_system.cpp

1361 lines
48 KiB
C++

#include "loki_name_system.h"
#include "checkpoints/checkpoints.h"
#include "common/loki.h"
#include "common/util.h"
#include "common/base32z.h"
#include "crypto/hash.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_core/cryptonote_tx_utils.h"
#include "cryptonote_basic/tx_extra.h"
#include "cryptonote_core/blockchain.h"
#include <lokimq/hex.h>
#include <sqlite3.h>
extern "C"
{
#include "sodium.h"
}
#undef LOKI_DEFAULT_LOG_CATEGORY
#define LOKI_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_mappings_on_height_and_newer,
get_owner,
get_setting,
get_sentinel_end,
};
enum struct lns_db_setting_row
{
id,
top_height,
top_hash,
version,
};
enum struct owner_record_row
{
id,
public_key,
};
enum struct mapping_record_row
{
id,
type,
name,
value,
txid,
prev_txid,
register_height,
owner_id,
_count,
};
static bool sql_copy_blob(sqlite3_stmt *statement, int row, void *dest, int dest_size)
{
void const *blob = sqlite3_column_blob(statement, row);
int blob_len = sqlite3_column_bytes(statement, row);
assert(blob_len == dest_size);
if (blob_len != dest_size)
{
LOG_PRINT_L0("Unexpected blob size=" << blob_len << ", in LNS DB does not match expected size=" << dest_size);
return false;
}
memcpy(dest, blob, blob_len);
return true;
}
static bool sql_run_statement(cryptonote::network_type nettype, lns_sql_type type, sqlite3_stmt *statement, void *context)
{
bool data_loaded = false;
bool result = false;
for (bool infinite_loop = true; infinite_loop;)
{
int step_result = sqlite3_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::get_owner:
{
auto *entry = reinterpret_cast<owner_record *>(context);
entry->id = sqlite3_column_int(statement, static_cast<int>(owner_record_row::id));
if (!sql_copy_blob(statement, static_cast<int>(owner_record_row::public_key), entry->key.data, sizeof(entry->key.data)))
return false;
data_loaded = true;
}
break;
case lns_sql_type::get_setting:
{
auto *entry = reinterpret_cast<settings_record *>(context);
entry->top_height = static_cast<uint64_t>(sqlite3_column_int64(statement, static_cast<int>(lns_db_setting_row::top_height)));
if (!sql_copy_blob(statement, static_cast<int>(lns_db_setting_row::top_hash), entry->top_hash.data, sizeof(entry->top_hash.data)))
return false;
entry->version = sqlite3_column_int(statement, static_cast<int>(lns_db_setting_row::version));
data_loaded = true;
}
break;
case lns_sql_type::get_mappings_on_height_and_newer: /* FALLTHRU */
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:
{
mapping_record tmp_entry = {};
int type_int = static_cast<uint16_t>(sqlite3_column_int(statement, static_cast<int>(mapping_record_row::type)));
if (type_int >= tools::enum_count<mapping_type>)
return false;
tmp_entry.type = static_cast<mapping_type>(type_int);
tmp_entry.register_height = static_cast<uint16_t>(sqlite3_column_int(statement, static_cast<int>(mapping_record_row::register_height)));
tmp_entry.owner_id = sqlite3_column_int(statement, static_cast<int>(mapping_record_row::owner_id));
int name_len = sqlite3_column_bytes(statement, static_cast<int>(mapping_record_row::name));
auto *name = reinterpret_cast<char const *>(sqlite3_column_text(statement, static_cast<int>(mapping_record_row::name)));
size_t value_len = static_cast<size_t>(sqlite3_column_bytes(statement, static_cast<int>(mapping_record_row::value)));
auto *value = reinterpret_cast<char const *>(sqlite3_column_text(statement, static_cast<int>(mapping_record_row::value)));
tmp_entry.name = std::string(name, name_len);
tmp_entry.value = std::string(value, value_len);
if (!sql_copy_blob(statement, static_cast<int>(mapping_record_row::txid), tmp_entry.txid.data, sizeof(tmp_entry.txid)))
return false;
if (!sql_copy_blob(statement, static_cast<int>(mapping_record_row::prev_txid), tmp_entry.prev_txid.data, sizeof(tmp_entry.prev_txid)))
return false;
int last_column = tools::enum_count<mapping_record_row> + 1;
if (!sql_copy_blob(statement, last_column, tmp_entry.owner.data, sizeof(tmp_entry.owner)))
return false;
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) <<", reason: " << sqlite3_errstr(step_result));
infinite_loop = false;
break;
}
}
}
sqlite3_reset(statement);
sqlite3_clear_bindings(statement);
return result;
}
bool mapping_record::active(cryptonote::network_type nettype, uint64_t blockchain_height) const
{
if (!loaded) return false;
if (type != mapping_type::lokinet) return true;
uint64_t expiry_blocks = lns::lokinet_expiry_blocks(nettype);
uint64_t const last_active_height = register_height + expiry_blocks;
return last_active_height >= (blockchain_height - 1);
}
static bool sql_compile_statement(sqlite3 *db, char const *query, int query_len, sqlite3_stmt **statement, bool optimise_for_multiple_usage = true)
{
#if SQLITE_VERSION_NUMBER >= 3020000
int prepare_result = sqlite3_prepare_v3(db, query, query_len, optimise_for_multiple_usage ? SQLITE_PREPARE_PERSISTENT : 0, statement, nullptr /*pzTail*/);
#else
int prepare_result = sqlite3_prepare_v2(db, query, query_len, statement, nullptr /*pzTail*/);
#endif
bool result = prepare_result == SQLITE_OK;
if (!result) MERROR("Can not compile SQL statement: " << query << ", reason: " << sqlite3_errstr(prepare_result));
return result;
}
burn_type mapping_type_to_burn_type(mapping_type in)
{
burn_type result = burn_type::custom;
switch (in)
{
case mapping_type::lokinet: result = burn_type::lokinet_1year; break;
case mapping_type::session: result = burn_type::session; break;
case mapping_type::wallet: result = burn_type::wallet; break;
default: break;
}
return result;
}
uint64_t burn_requirement_in_atomic_loki(uint8_t /*hf_version*/, burn_type type)
{
uint64_t result = 0;
switch (type)
{
case burn_type::update_record:
result = 0;
break;
case burn_type::lokinet_1year: /* FALLTHRU */
case burn_type::session: /* FALLTHRU */
case burn_type::wallet: /* FALLTHRU */
case burn_type::custom: /* FALLTHRU */
default: result = 30 * COIN;
break;
}
return result;
}
sqlite3 *init_loki_name_system(char const *file_path)
{
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 sql_open = sqlite3_open(file_path, &result);
if (sql_open != SQLITE_OK)
{
MERROR("Failed to open LNS db at: " << file_path << ", reason: " << sqlite3_errstr(sql_init));
return nullptr;
}
return result;
}
uint64_t lokinet_expiry_blocks(cryptonote::network_type nettype, uint64_t *renew_window)
{
uint64_t renew_window_ = BLOCKS_EXPECTED_IN_DAYS(31);
uint64_t result = BLOCKS_EXPECTED_IN_YEARS(1) + renew_window_;
if (nettype == cryptonote::FAKECHAIN)
{
renew_window_ = 10;
result = 10 + renew_window_;
}
else if (nettype == cryptonote::TESTNET)
{
renew_window_ = BLOCKS_EXPECTED_IN_DAYS(1);
result = BLOCKS_EXPECTED_IN_DAYS(1) + renew_window_;
}
if (renew_window) *renew_window = renew_window_;
return result;
}
crypto::hash tx_extra_signature_hash(epee::span<const uint8_t> blob, 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");
crypto::hash result = {};
if (blob.size() <= lns::GENERIC_VALUE_MAX)
{
unsigned char buffer[lns::GENERIC_VALUE_MAX + sizeof(prev_txid)] = {};
size_t buffer_len = blob.size() + sizeof(prev_txid);
memcpy(buffer, blob.data(), blob.size());
memcpy(buffer + blob.size(), prev_txid.data, sizeof(prev_txid));
crypto_generichash(reinterpret_cast<unsigned char *>(result.data), sizeof(result), buffer, buffer_len, NULL /*key*/, 0 /*key_len*/);
}
else
{
MERROR("Unexpected blob len=" << blob.size() << " greater than the blob backing buffer capacity=" << lns::GENERIC_VALUE_MAX);
}
return result;
}
static bool char_is_num(char c)
{
bool result = c >= '0' && c <= '9';
return result;
}
static bool char_is_alpha(char c)
{
bool result = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
return result;
}
static bool char_is_alphanum(char c)
{
bool result = char_is_num(c) || char_is_alpha(c);
return result;
}
bool validate_lns_name(mapping_type type, std::string const &name, std::string *reason)
{
std::stringstream err_stream;
LOKI_DEFER { if (reason) *reason = err_stream.str(); };
size_t max_name_len = lns::GENERIC_NAME_MAX;
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 (type == mapping_type::lokinet) max_name_len = lns::LOKINET_DOMAIN_NAME_MAX;
// NOTE: Validate name length
if (name.empty() || name.size() > max_name_len)
{
if (reason)
{
err_stream << "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;
}
// NOTE: Validate domain specific requirements
if (type == mapping_type::session)
{
}
else if (type == mapping_type::lokinet)
{
// Domain has to start with a letter or digit, and can have letters, digits, or hyphens in between and must end with a .loki
// ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.loki$
{
char const SHORTEST_DOMAIN[] = "a.loki";
if (name.size() < static_cast<int>(loki::char_count(SHORTEST_DOMAIN)))
{
if (reason)
{
err_stream << "LNS type=lokinet, specifies mapping from name -> value where the name is shorter than the shortest possible name=" << SHORTEST_DOMAIN << ", given name=" << name;
}
return false;
}
}
if (!char_is_alphanum(name[0])) // Must start with alphanumeric
{
if (reason)
{
err_stream << "LNS type=lokinet, specifies mapping from name -> value where the name does not start with an alphanumeric character, name=" << name;
}
return false;
}
char const SUFFIX[] = ".loki";
char const *name_suffix = name.data() + (name.size() - loki::char_count(SUFFIX));
if (memcmp(name_suffix, SUFFIX, loki::char_count(SUFFIX)) != 0) // Must end with .loki
{
if (reason)
{
err_stream << "LNS type=lokinet, specifies mapping from name -> value where the name does not end with the domain .loki, name=" << name;
}
return false;
}
char const *char_preceeding_suffix = name_suffix - 1;
if (!char_is_alphanum(char_preceeding_suffix[0])) // Characted preceeding suffix must be alphanumeric
{
if (reason)
{
err_stream << "LNS type=lokinet, specifies mapping from name -> value where the character preceeding the <char>.loki is not alphanumeric, char=" << char_preceeding_suffix[0] << ", name=" << name;
}
return false;
}
char const *begin = name.data() + 1;
char const *end = char_preceeding_suffix;
for (char const *it = begin; it < end; it++) // Inbetween start and preceeding suffix, alphanumeric and hyphen characters permitted
{
char c = it[0];
if (!(char_is_alphanum(c) || c == '-'))
{
if (reason)
{
err_stream << "LNS type=lokinet, specifies mapping from name -> value where the domain name contains more than the permitted alphanumeric or hyphen characters name=" << name;
}
return false;
}
}
}
return true;
}
static bool check_lengths(mapping_type type, std::string const &value, size_t max, bool require_exact_len, std::string *reason)
{
bool result = true;
if (require_exact_len)
{
if (value.size() != max)
result = false;
}
else
{
if (value.size() > max || value.size() == 0)
{
result = false;
}
}
if (!result)
{
if (reason)
{
std::stringstream err_stream;
err_stream << "LNS type=" << type << ", specifies mapping from name -> value where the value's length=" << value.size();
if (require_exact_len) err_stream << ", does not equal the required length=";
else err_stream <<" is 0 or exceeds the maximum length=";
err_stream << max << ", given value=" << value;
*reason = err_stream.str();
}
}
return result;
}
bool validate_lns_value(cryptonote::network_type nettype, mapping_type type, std::string const &value, lns_value *blob, std::string *reason)
{
if (blob) *blob = {};
std::stringstream err_stream;
cryptonote::address_parse_info addr_info = {};
static_assert(GENERIC_VALUE_MAX >= SESSION_PUBLIC_KEY_BINARY_LENGTH, "lns_value assumes the largest blob size required, all other values should be able to fit into this buffer");
static_assert(GENERIC_VALUE_MAX >= LOKINET_ADDRESS_BINARY_LENGTH, "lns_value assumes the largest blob size required, all other values should be able to fit into this buffer");
static_assert(GENERIC_VALUE_MAX >= sizeof(addr_info.address), "lns_value assumes the largest blob size required, all other values should be able to fit into this buffer");
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;
}
}
else
{
int max_value_len = lns::GENERIC_VALUE_MAX;
bool value_require_exact_len = true;
if (type == mapping_type::lokinet) max_value_len = (LOKINET_ADDRESS_BINARY_LENGTH * 2);
else if (type == mapping_type::session) max_value_len = (SESSION_PUBLIC_KEY_BINARY_LENGTH * 2);
else value_require_exact_len = false;
if (!check_lengths(type, value, max_value_len, value_require_exact_len, reason))
return false;
}
if (type == mapping_type::wallet)
{
if (blob)
{
blob->len = sizeof(addr_info.address);
memcpy(blob->buffer.data(), &addr_info.address, blob->len);
}
}
else if (type == mapping_type::lokinet)
{
if (value.size() != 52)
{
if (reason)
{
err_stream << "The lokinet value=" << value << ", should be a 52 char base32z string, length=" << value.size();
*reason = err_stream.str();
}
return false;
}
crypto::ed25519_public_key pkey;
if (!base32z::decode(value, pkey))
{
if (reason)
{
err_stream << "The value=" << value << ", was not a decodable base32z value.";
*reason = err_stream.str();
}
return false;
}
if (blob)
{
blob->len = sizeof(pkey);
memcpy(blob->buffer.data(), pkey.data, blob->len);
}
}
else
{
// NOTE: Check value is hex
if ((value.size() % 2) != 0)
{
if (reason)
{
err_stream << "The value=" << value << ", should be a hex string that has an even length to be convertible back into binary, length=" << value.size();
*reason = err_stream.str();
}
return false;
}
if (!lokimq::is_hex(value))
{
if (reason)
{
err_stream << "LNS type=" << type <<", specifies name -> value mapping where the value is not a hex string given value=" << value;
*reason = err_stream.str();
}
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());
}
if (type == mapping_type::session)
{
if (!(value[0] == '0' && value[1] == '5')) // NOTE: Session public keys are 33 bytes, with the first byte being 0x05 and the remaining 32 being the public key.
{
if (reason)
{
err_stream << "LNS type=session, specifies mapping from name -> ed25519 key where the key is not prefixed with 53 (0x05), prefix=" << std::to_string(value[0]) << " (" << value[0] << "), given ed25519=" << value;
*reason = err_stream.str();
}
return false;
}
}
}
return true;
}
bool validate_lns_value_binary(mapping_type type, std::string const &value, std::string *reason)
{
int max_value_len = lns::GENERIC_VALUE_MAX;
bool value_require_exact_len = true;
if (type == mapping_type::lokinet) max_value_len = LOKINET_ADDRESS_BINARY_LENGTH;
else if (type == mapping_type::session) max_value_len = SESSION_PUBLIC_KEY_BINARY_LENGTH;
else if (type == mapping_type::wallet) max_value_len = sizeof(cryptonote::account_public_address);
else value_require_exact_len = false;
if (!check_lengths(type, value, max_value_len, value_require_exact_len, reason))
return false;
if (type == lns::mapping_type::wallet)
{
// TODO(doyle): Better address validation? Is it a valid address, is it a valid nettype address?
cryptonote::account_public_address address;
memcpy(&address, value.data(), sizeof(address));
if (!(crypto::check_key(address.m_spend_public_key) && crypto::check_key(address.m_view_public_key)))
{
if (reason)
{
std::stringstream err_stream;
err_stream << "LNS type=" << type << ", specifies mapping from name -> wallet address where the wallet address's blob, does not generate valid public spend/view keys";
*reason = err_stream.str();
}
return false;
}
}
return true;
}
static std::ostream &operator<<(std::ostream &stream, cryptonote::tx_extra_loki_name_system const &data)
{
stream << "LNS Extra={";
if (data.command == lns::tx_command_t::buy)
stream << "owner=" << data.owner;
else
stream << "signature=" << epee::string_tools::pod_to_hex(data.signature);
stream << ", type=" << data.type << ", name=" << data.name << "}";
return stream;
}
static bool validate_against_previous_mapping(lns::name_system_db const &lns_db, uint64_t blockchain_height, cryptonote::transaction const &tx, cryptonote::tx_extra_loki_name_system &data, std::string *reason = nullptr)
{
std::stringstream err_stream;
crypto::hash expected_prev_txid = crypto::null_hash;
lns::mapping_record mapping = lns_db.get_mapping(data.type, data.name);
const bool updating = data.command == lns::tx_command_t::update;
if (updating && !mapping)
{
if (reason)
{
err_stream << tx << ", " << data << ", update requested but mapping does not exist.";
*reason = err_stream.str();
}
return false;
}
if (mapping)
{
expected_prev_txid = mapping.txid;
if (updating)
{
if (data.type == lns::mapping_type::lokinet && !mapping.active(lns_db.network_type(), blockchain_height))
{
// Updating, we can always update unless the mapping has expired
if (reason)
{
err_stream << tx << ", " << data << ", TX requested to update mapping that has already expired";
*reason = err_stream.str();
}
return false;
}
if (data.value == mapping.value)
{
if (reason)
{
err_stream << tx << ", " << data << ", value to update to is already the same as the mapping value";
*reason = err_stream.str();
}
return false;
}
// Validate signature
{
crypto::hash hash = tx_extra_signature_hash(epee::span<const uint8_t>(reinterpret_cast<const uint8_t *>(data.value.data()), data.value.size()), expected_prev_txid);
if (crypto_sign_verify_detached(data.signature.data, reinterpret_cast<unsigned char *>(hash.data), sizeof(hash.data), mapping.owner.data) != 0)
{
if (reason)
{
err_stream << tx << ", " << data << ", failed to verify signature for LNS update";
*reason = err_stream.str();
}
return false;
}
}
data.owner = mapping.owner;
}
else
{
if (data.type != lns::mapping_type::lokinet)
{
if (reason)
{
lns::owner_record owner = lns_db.get_owner_by_id(mapping.owner_id);
err_stream << tx << ", " << data << ", non-lokinet entries can NOT be renewed, mapping already exists with name=" << mapping.name << ", owner=" << owner.key << ", type=" << mapping.type;
*reason = err_stream.str();
}
return false;
}
uint64_t renew_window = 0;
uint64_t expiry_blocks = lns::lokinet_expiry_blocks(lns_db.network_type(), &renew_window);
uint64_t const renew_window_offset = expiry_blocks - renew_window;
uint64_t const min_renew_height = mapping.register_height + renew_window_offset;
if (min_renew_height >= blockchain_height)
{
err_stream << tx << ", " << data << ", trying to renew too early, the earliest renew height=" << min_renew_height << ", urrent height=" << blockchain_height;
*reason = err_stream.str();
return false; // Trying to renew too early
}
if (mapping.active(lns_db.network_type(), blockchain_height))
{
// Lokinet entry expired i.e. it's no longer active. A purchase for this name is valid
// Check that the request originates from the owner of this mapping
lns::owner_record const requester = lns_db.get_owner_by_key(data.owner);
if (!requester)
{
if (reason)
{
err_stream << tx << ", " << data << ", trying to renew existing mapping but owner specified in LNS extra does not exist, rejected";
*reason = err_stream.str();
}
return false;
}
lns::owner_record const owner = lns_db.get_owner_by_id(mapping.owner_id);
if (!owner)
{
if (reason)
{
err_stream << tx << ", " << data << ", unexpected owner_id=" << mapping.owner_id << " does not exist";
*reason = err_stream.str();
}
return false;
}
if (requester.id != owner.id)
{
if (reason)
{
err_stream << tx << ", " << data << ", actual owner=" << owner.key << ", with owner_id=" << mapping.owner_id << ", does not match requester=" << requester.key << ", with id=" << requester.id;
*reason = err_stream.str();
}
return false;
}
}
// else mapping has expired, new purchase is valid
}
}
if (data.prev_txid != expected_prev_txid)
{
if (reason)
{
err_stream << tx << ", " << data << ", specified prior owner txid=" << data.prev_txid << ", but LNS DB reports=" << expected_prev_txid << ", possible competing TX was submitted and accepted before this TX was processed";
*reason = err_stream.str();
}
return false;
}
return true;
}
bool name_system_db::validate_lns_tx(uint8_t hf_version, uint64_t blockchain_height, cryptonote::transaction const &tx, cryptonote::tx_extra_loki_name_system *entry, std::string *reason) const
{
cryptonote::tx_extra_loki_name_system entry_;
if (!entry) entry = &entry_;
std::stringstream err_stream;
if (tx.type != cryptonote::txtype::loki_name_system)
{
if (reason)
{
err_stream << tx << ", uses wrong tx type, expected=" << cryptonote::txtype::loki_name_system;
*reason = err_stream.str();
}
return false;
}
if (!cryptonote::get_loki_name_system_from_tx_extra(tx.extra, *entry))
{
if (reason)
{
err_stream << tx << ", didn't have loki name service in the tx_extra";
*reason = err_stream.str();
}
return false;
}
if (entry->version != 0)
{
if (reason)
{
err_stream << tx << ", " << *entry << ", unexpected version=" << std::to_string(entry->version) << ", expected=0";
*reason = err_stream.str();
}
return false;
}
if (!lns::mapping_type_allowed(hf_version, entry->type))
{
if (reason)
{
err_stream << tx << ", " << *entry << ", specifying type=" << entry->type << " that is disallowed";
*reason = err_stream.str();
}
return false;
}
if (!validate_lns_name(entry->type, entry->name, reason))
return false;
if (!validate_lns_value_binary(entry->type, entry->value, reason))
return false;
if (!validate_against_previous_mapping(*this, blockchain_height, tx, *entry, reason))
return false;
uint64_t burn = cryptonote::get_burned_amount_from_tx_extra(tx.extra);
uint64_t const burn_required = entry->command == lns::tx_command_t::buy
? burn_requirement_in_atomic_loki(hf_version, mapping_type_to_burn_type(entry->type))
: 0;
if (burn != burn_required)
{
if (reason)
{
char const *over_or_under = burn > burn_required ? "too much " : "insufficient ";
err_stream << tx << ", " << *entry << ", burned " << over_or_under << "loki=" << burn << ", require=" << burn_required;
*reason = err_stream.str();
}
return false;
}
return true;
}
bool validate_mapping_type(std::string const &type, lns::mapping_type *mapping_type, std::string *reason)
{
std::string type_lowered = type;
for (char &ch : type_lowered)
{
if (ch >= 'A' && ch <= 'Z')
ch = ch + ('a' - 'A');
}
lns::mapping_type mapping_type_ = lns::mapping_type::session;
if (type_lowered == "session") mapping_type_ = lns::mapping_type::session;
else
{
try
{
size_t value = std::stoul(type_lowered);
if (value > tools::enum_count<lns::mapping_type>)
{
if (reason) *reason = "LNS type specifies value too large, must be from [0-" + std::to_string(tools::enum_count<lns::mapping_type>) + "): " + std::to_string(value);
return false;
}
mapping_type_ = static_cast<lns::mapping_type>(value);
}
catch (std::exception const &)
{
if (reason)
{
*reason = "Failed to convert lns mapping (was not proper integer, or not one of the recognised: \"session\"), string was";
if (type.empty())
{
*reason += " empty.";
}
else
{
*reason += "=";
*reason += type;
}
}
return false;
}
}
if (mapping_type) *mapping_type = mapping_type_;
return true;
}
static bool build_default_tables(sqlite3 *db)
{
constexpr char BUILD_TABLE_SQL[] = R"(
CREATE TABLE IF NOT EXISTS "owner"(
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"public_key" 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
);
CREATE TABLE IF NOT EXISTS "mappings" (
"id" INTEGER PRIMARY KEY NOT NULL,
"type" INTEGER NOT NULL,
"name" VARCHAR NOT NULL,
"value" BLOB NOT NULL,
"txid" BLOB NOT NULL,
"prev_txid" BLOB NOT NULL,
"register_height" INTEGER NOT NULL,
"owner_id" INTEGER NOT NULL REFERENCES "owner" ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "name_type_id" ON mappings("name", "type");
)";
char *table_err_msg = nullptr;
int table_created = sqlite3_exec(db, BUILD_TABLE_SQL, 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;
}
return true;
}
enum struct db_version { v1, };
auto constexpr DB_VERSION = db_version::v1;
bool name_system_db::init(cryptonote::network_type nettype, sqlite3 *db, uint64_t top_height, crypto::hash const &top_hash)
{
if (!db) return false;
this->db = db;
this->nettype = nettype;
char constexpr GET_MAPPINGS_BY_OWNER_SQL[] = R"(SELECT * FROM "mappings" JOIN "owner" ON "mappings"."owner_id" = "owner"."id" WHERE "public_key" = ?)";
char constexpr GET_MAPPINGS_ON_HEIGHT_AND_NEWER_SQL[] = R"(SELECT * FROM "mappings" JOIN "owner" on "mappings"."owner_id" = "owner"."id" WHERE "register_height" >= ?)";
char constexpr GET_MAPPING_SQL[] = R"(SELECT * FROM "mappings" JOIN "owner" on "mappings"."owner_id" = "owner"."id" WHERE "type" = ? AND "name" = ?)";
char constexpr GET_OWNER_BY_ID_SQL[] = R"(SELECT * FROM "owner" WHERE "id" = ?)";
char constexpr GET_OWNER_BY_KEY_SQL[] = R"(SELECT * FROM "owner" WHERE "public_key" = ?)";
char constexpr GET_SETTINGS_SQL[] = R"(SELECT * FROM "settings" WHERE "id" = 1)";
char constexpr PRUNE_MAPPINGS_SQL[] = R"(DELETE FROM "mappings" WHERE "register_height" >= ?)";
char constexpr PRUNE_OWNERS_SQL[] = R"(DELETE FROM "owner" WHERE "id" NOT IN (SELECT "owner_id" FROM "mappings"))";
char constexpr SAVE_MAPPING_SQL[] = R"(INSERT OR REPLACE INTO "mappings" ("type", "name", "value", "txid", "prev_txid", "register_height", "owner_id") VALUES (?,?,?,?,?,?,?))";
char constexpr SAVE_OWNER_SQL[] = R"(INSERT INTO "owner" ("public_key") VALUES (?);)";
char constexpr SAVE_SETTINGS_SQL[] = R"(INSERT OR REPLACE INTO "settings" ("id", "top_height", "top_hash", "version") VALUES (1,?,?,?))";
sqlite3_stmt *test;
if (!build_default_tables(db))
return false;
if (
!sql_compile_statement(db, GET_MAPPINGS_BY_OWNER_SQL, loki::array_count(GET_MAPPINGS_BY_OWNER_SQL), &get_mappings_by_owner_sql) ||
!sql_compile_statement(db, GET_MAPPINGS_ON_HEIGHT_AND_NEWER_SQL, loki::array_count(GET_MAPPINGS_ON_HEIGHT_AND_NEWER_SQL), &get_mappings_on_height_and_newer_sql) ||
!sql_compile_statement(db, GET_MAPPING_SQL, loki::array_count(GET_MAPPING_SQL), &get_mapping_sql) ||
!sql_compile_statement(db, GET_SETTINGS_SQL, loki::array_count(GET_SETTINGS_SQL), &get_settings_sql) ||
!sql_compile_statement(db, GET_OWNER_BY_ID_SQL, loki::array_count(GET_OWNER_BY_ID_SQL), &get_owner_by_id_sql) ||
!sql_compile_statement(db, GET_OWNER_BY_KEY_SQL, loki::array_count(GET_OWNER_BY_KEY_SQL), &get_owner_by_key_sql) ||
!sql_compile_statement(db, PRUNE_MAPPINGS_SQL, loki::array_count(PRUNE_MAPPINGS_SQL), &prune_mappings_sql) ||
!sql_compile_statement(db, PRUNE_OWNERS_SQL, loki::array_count(PRUNE_OWNERS_SQL), &prune_owners_sql) ||
!sql_compile_statement(db, SAVE_MAPPING_SQL, loki::array_count(SAVE_MAPPING_SQL), &save_mapping_sql) ||
!sql_compile_statement(db, SAVE_SETTINGS_SQL, loki::array_count(SAVE_SETTINGS_SQL), &save_settings_sql) ||
!sql_compile_statement(db, SAVE_OWNER_SQL, loki::array_count(SAVE_OWNER_SQL), &save_owner_sql)
)
{
return false;
}
if (settings_record settings = get_settings())
{
if (settings.top_height == top_height && settings.top_hash == top_hash)
{
this->last_processed_height = settings.top_height;
assert(settings.version == static_cast<int>(DB_VERSION));
}
else
{
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(db)) return false;
}
}
return true;
}
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;
}
static bool add_lns_entry(lns::name_system_db &lns_db, uint64_t height, cryptonote::tx_extra_loki_name_system const &entry, crypto::hash const &tx_hash)
{
int64_t owner_id = 0;
if (owner_record owner = lns_db.get_owner_by_key(entry.owner)) owner_id = owner.id;
if (owner_id == 0)
{
if (entry.command == lns::tx_command_t::update)
{
MERROR("Owner does not exist but TX received is trying to update an existing mapping (i.e. owner should already exist). TX=" << tx_hash << " should have failed validation prior.");
return false;
}
if (!lns_db.save_owner(entry.owner, &owner_id))
{
LOG_PRINT_L1("Failed to save LNS owner to DB tx: " << tx_hash << ", type: " << entry.type << ", name: " << entry.name << ", owner: " << entry.owner);
return false;
}
}
assert(owner_id != 0);
if (!lns_db.save_mapping(tx_hash, entry, height, owner_id))
{
LOG_PRINT_L1("Failed to save LNS entry to DB tx: " << tx_hash << ", type: " << entry.type << ", name: " << entry.name << ", owner: " << entry.owner);
return false;
}
return true;
}
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;
if (block.major_version >= cryptonote::network_version_15_lns)
{
for (cryptonote::transaction const &tx : txs)
{
if (tx.type != cryptonote::txtype::loki_name_system)
continue;
cryptonote::tx_extra_loki_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;
}
}
last_processed_height = height;
save_settings(height, cryptonote::get_block_hash(block), static_cast<int>(DB_VERSION));
db_transaction.commit = true;
return true;
}
static bool get_txid_lns_entry(cryptonote::Blockchain const &blockchain, crypto::hash txid, cryptonote::tx_extra_loki_name_system &extra)
{
std::vector<cryptonote::transaction> txs;
std::vector<crypto::hash> missed_txs;
if (!blockchain.get_transactions({txid}, txs, missed_txs) || txs.empty())
return false;
return cryptonote::get_loki_name_system_from_tx_extra(txs[0].extra, extra);
}
static bool find_closest_valid_lns_tx_extra_in_blockchain(cryptonote::Blockchain const &blockchain,
lns::mapping_record const &mapping,
uint64_t blockchain_height,
cryptonote::tx_extra_loki_name_system &extra,
crypto::hash &tx_hash,
uint64_t &extra_height)
{
uint64_t prev_height = static_cast<uint64_t>(-1);
crypto::hash prev_txid = mapping.prev_txid;
cryptonote::tx_extra_loki_name_system prev_entry = {};
if (!get_txid_lns_entry(blockchain, prev_txid, prev_entry)) return false;
for (;;)
{
std::vector<uint64_t> prev_heights = blockchain.get_transactions_heights({prev_txid});
if (prev_heights.empty())
{
MERROR("Unexpected error querying TXID=" << prev_txid << ", height from DB for LNS");
return false;
}
prev_height = prev_heights[0];
if (prev_height >= blockchain_height)
{
// Previous owner of mapping is after the detach height, iterate back to
// next prev entry by getting the relevant transaction, extract the LNS
// extra and continue searching.
if (!get_txid_lns_entry(blockchain, prev_txid, prev_entry))
return false;
prev_txid = prev_entry.prev_txid;
}
else
{
break;
}
}
bool result = prev_height < blockchain_height && prev_txid != crypto::null_hash;
if (result)
{
tx_hash = prev_txid;
extra = std::move(prev_entry);
extra_height = prev_height;
}
return result;
}
void name_system_db::block_detach(cryptonote::Blockchain const &blockchain, uint64_t height)
{
std::vector<mapping_record> new_mappings = {};
{
sqlite3_stmt *statement = get_mappings_on_height_and_newer_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_int(statement, 1 /*sql param index*/, height);
sql_run_statement(nettype, lns_sql_type::get_mappings_on_height_and_newer, statement, &new_mappings);
}
struct lns_parts
{
uint64_t height;
crypto::hash tx_hash;
cryptonote::tx_extra_loki_name_system entry;
};
std::vector<lns_parts> entries;
for (auto const &mapping : new_mappings)
{
cryptonote::tx_extra_loki_name_system entry = {};
uint64_t entry_height = 0;
crypto::hash tx_hash = {};
if (!find_closest_valid_lns_tx_extra_in_blockchain(blockchain, mapping, height, entry, tx_hash, entry_height)) continue;
entries.push_back({entry_height, tx_hash, entry});
}
prune_db(height);
for (auto const &lns : entries)
{
if (!add_lns_entry(*this, lns.height, lns.entry, lns.tx_hash))
MERROR("Unexpected failure to add historical LNS into the DB on reorganization from tx=" << lns.tx_hash);
}
}
bool name_system_db::save_owner(crypto::ed25519_public_key const &key, int64_t *row_id)
{
sqlite3_stmt *statement = save_owner_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_blob(statement, 1 /*sql param index*/, &key, sizeof(key), nullptr /*destructor*/);
bool result = sql_run_statement(nettype, lns_sql_type::save_owner, statement, nullptr);
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_loki_name_system const &src, uint64_t height, int64_t owner_id)
{
sqlite3_stmt *statement = save_mapping_sql;
sqlite3_bind_int (statement, static_cast<int>(mapping_record_row::type), static_cast<uint16_t>(src.type));
sqlite3_bind_text (statement, static_cast<int>(mapping_record_row::name), src.name.data(), src.name.size(), nullptr /*destructor*/);
sqlite3_bind_blob (statement, static_cast<int>(mapping_record_row::value), src.value.data(), src.value.size(), nullptr /*destructor*/);
sqlite3_bind_blob (statement, static_cast<int>(mapping_record_row::txid), tx_hash.data, sizeof(tx_hash), nullptr /*destructor*/);
sqlite3_bind_blob (statement, static_cast<int>(mapping_record_row::prev_txid), src.prev_txid.data, sizeof(src.prev_txid), nullptr /*destructor*/);
sqlite3_bind_int64(statement, static_cast<int>(mapping_record_row::register_height), static_cast<int64_t>(height));
sqlite3_bind_int64(statement, static_cast<int>(mapping_record_row::owner_id), owner_id);
bool result = sql_run_statement(nettype, 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)
{
sqlite3_stmt *statement = save_settings_sql;
sqlite3_bind_int64(statement, static_cast<int>(lns_db_setting_row::top_height), top_height);
sqlite3_bind_blob (statement, static_cast<int>(lns_db_setting_row::top_hash), top_hash.data, sizeof(top_hash), nullptr /*destructor*/);
sqlite3_bind_int (statement, static_cast<int>(lns_db_setting_row::version), version);
bool result = sql_run_statement(nettype, lns_sql_type::save_setting, statement, nullptr);
return result;
}
bool name_system_db::prune_db(uint64_t height)
{
{
sqlite3_stmt *statement = prune_mappings_sql;
sqlite3_bind_int64(statement, 1 /*sql param index*/, height);
if (!sql_run_statement(nettype, lns_sql_type::pruning, statement, nullptr)) return false;
}
{
sqlite3_stmt *statement = prune_owners_sql;
if (!sql_run_statement(nettype, lns_sql_type::pruning, statement, nullptr)) return false;
}
return true;
}
owner_record name_system_db::get_owner_by_key(crypto::ed25519_public_key const &key) const
{
sqlite3_stmt *statement = get_owner_by_key_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_blob(statement, 1 /*sql param index*/, &key, sizeof(key), nullptr /*destructor*/);
owner_record result = {};
result.loaded = sql_run_statement(nettype, lns_sql_type::get_owner, statement, &result);
return result;
}
owner_record name_system_db::get_owner_by_id(int64_t owner_id) const
{
sqlite3_stmt *statement = get_owner_by_id_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_int(statement, 1 /*sql param index*/, owner_id);
owner_record result = {};
result.loaded = sql_run_statement(nettype, lns_sql_type::get_owner, statement, &result);
return result;
}
mapping_record name_system_db::get_mapping(mapping_type type, std::string const &name) const
{
sqlite3_stmt *statement = get_mapping_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_int(statement, 1 /*sql param index*/, static_cast<int>(type));
sqlite3_bind_text(statement, 2 /*sql param index*/, name.data(), name.size(), nullptr /*destructor*/);
mapping_record result = {};
result.loaded = sql_run_statement(nettype, lns_sql_type::get_mapping, statement, &result);
return result;
}
std::vector<mapping_record> name_system_db::get_mappings(std::vector<uint16_t> const &types, std::string const &name) const
{
std::string sql_statement;
// Generate string statement
{
char constexpr SQL_PREFIX[] = R"(SELECT * FROM "mappings" JOIN "owner" ON "mappings"."owner_id" = "owner"."id" WHERE "name" = ? AND "type" in ()";
char constexpr SQL_SUFFIX[] = R"())";
std::stringstream stream;
stream << SQL_PREFIX;
for (size_t i = 0; i < types.size(); i++)
{
stream << "?";
if (i < (types.size() - 1)) stream << ", ";
}
stream << SQL_SUFFIX;
sql_statement = stream.str();
}
// Compile Statement
std::vector<mapping_record> result;
sqlite3_stmt *statement = nullptr;
if (!sql_compile_statement(db, sql_statement.c_str(), sql_statement.size(), &statement, false /*optimise_for_multiple_usage*/))
return result;
// Bind parameters statements
int sql_param_index = 1;
sqlite3_bind_text(statement, sql_param_index++, name.data(), name.size(), nullptr /*destructor*/);
for (auto type : types)
sqlite3_bind_int(statement, sql_param_index++, type);
assert((sql_param_index - 1) == static_cast<int>(1 /*name*/ + types.size()));
// Execute
sql_run_statement(nettype, lns_sql_type::get_mappings, statement, &result);
return result;
}
std::vector<mapping_record> name_system_db::get_mappings_by_owners(std::vector<crypto::ed25519_public_key> const &keys) const
{
std::string sql_statement;
// Generate string statement
{
char constexpr SQL_PREFIX[] = R"(SELECT * FROM "mappings" JOIN "owner" ON "mappings"."owner_id" = "owner"."id" WHERE "public_key" in ()";
char constexpr SQL_SUFFIX[] = R"())";
std::stringstream stream;
stream << SQL_PREFIX;
for (size_t i = 0; i < keys.size(); i++)
{
stream << "?";
if (i < (keys.size() - 1)) stream << ", ";
}
stream << SQL_SUFFIX;
sql_statement = stream.str();
}
// Compile Statement
std::vector<mapping_record> result;
sqlite3_stmt *statement = nullptr;
if (!sql_compile_statement(db, sql_statement.c_str(), sql_statement.size(), &statement, false /*optimise_for_multiple_usage*/))
return result;
// Bind parameters statements
int sql_param_index = 1;
for (auto const &key : keys)
sqlite3_bind_blob(statement, sql_param_index++, key.data, sizeof(key), nullptr /*destructor*/);
assert((sql_param_index - 1) == static_cast<int>(keys.size()));
// Execute
sql_run_statement(nettype, lns_sql_type::get_mappings_by_owners, statement, &result);
return result;
}
std::vector<mapping_record> name_system_db::get_mappings_by_owner(crypto::ed25519_public_key const &key) const
{
std::vector<mapping_record> result = {};
sqlite3_stmt *statement = get_mappings_by_owner_sql;
sqlite3_clear_bindings(statement);
sqlite3_bind_blob(statement, 1 /*sql param index*/, key.data, sizeof(key), nullptr /*destructor*/);
sql_run_statement(nettype, lns_sql_type::get_mappings_by_owner, statement, &result);
return result;
}
settings_record name_system_db::get_settings() const
{
sqlite3_stmt *statement = get_settings_sql;
settings_record result = {};
result.loaded = sql_run_statement(nettype, lns_sql_type::get_setting, statement, &result);
return result;
}
}; // namespace service_nodes