mirror of
https://github.com/vtnerd/monero-lws.git
synced 2025-03-23 07:28:45 +00:00
3066 lines
106 KiB
C++
3066 lines
106 KiB
C++
// Copyright (c) 2018-2023, The Monero Project
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without modification, are
|
|
// permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
|
// conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
|
// of conditions and the following disclaimer in the documentation and/or other
|
|
// materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
|
// used to endorse or promote products derived from this software without specific
|
|
// prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
#include "storage.h"
|
|
|
|
#include <boost/container/static_vector.hpp>
|
|
#include <boost/core/demangle.hpp>
|
|
#include <boost/range/adaptor/reversed.hpp>
|
|
#include <boost/range/adaptor/transformed.hpp>
|
|
#include <boost/range/counting_range.hpp>
|
|
#include <boost/range/iterator_range.hpp>
|
|
#include <boost/uuid/uuid_hash.hpp>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include "checkpoints/checkpoints.h"
|
|
#include "config.h"
|
|
#include "crypto/crypto.h"
|
|
#include "cryptonote_basic/cryptonote_basic.h"
|
|
#include "cryptonote_core/cryptonote_tx_utils.h"
|
|
#include "db/account.h"
|
|
#include "db/string.h"
|
|
#include "error.h"
|
|
#include "hex.h"
|
|
#include "lmdb/database.h"
|
|
#include "lmdb/error.h"
|
|
#include "lmdb/key_stream.h"
|
|
#include "lmdb/msgpack_table.h"
|
|
#include "lmdb/table.h"
|
|
#include "lmdb/util.h"
|
|
#include "lmdb/value_stream.h"
|
|
#include "net/net_parse_helpers.h" // monero/contrib/epee/include
|
|
#include "span.h"
|
|
#include "wire/adapted/array.h"
|
|
#include "wire/filters.h"
|
|
#include "wire/json.h"
|
|
#include "wire/vector.h"
|
|
#include "wire/wrapper/array.h"
|
|
#include "wire/wrappers_impl.h"
|
|
|
|
namespace wire
|
|
{
|
|
template<typename T, typename C>
|
|
static bool operator<(const array_<T, C>& lhs, const array_<T, C>& rhs)
|
|
{
|
|
return lhs.get_container() < rhs.get_container();
|
|
}
|
|
}
|
|
|
|
namespace lws
|
|
{
|
|
namespace db
|
|
{
|
|
namespace v0
|
|
{
|
|
//! Orignal DB value, with no txn fee
|
|
struct output
|
|
{
|
|
transaction_link link; //! Orders and links `output` to `spend`s.
|
|
|
|
//! Data that a linked `spend` needs in some REST endpoints.
|
|
struct spend_meta_
|
|
{
|
|
output_id id; //!< Unique id for output within monero
|
|
// `link` and `id` must be in this order for LMDB optimizations
|
|
std::uint64_t amount;
|
|
std::uint32_t mixin_count;//!< Ring-size of TX
|
|
std::uint32_t index; //!< Offset within a tx
|
|
crypto::public_key tx_public;
|
|
} spend_meta;
|
|
|
|
std::uint64_t timestamp;
|
|
std::uint64_t unlock_time; //!< Not always a timestamp; mirrors chain value.
|
|
crypto::hash tx_prefix_hash;
|
|
crypto::public_key pub; //!< One-time spendable public key.
|
|
rct::key ringct_mask; //!< Unencrypted CT mask
|
|
char reserved[7];
|
|
extra_and_length extra; //!< Extra info + length of payment id
|
|
union payment_id_
|
|
{
|
|
crypto::hash8 short_; //!< Decrypted short payment id
|
|
crypto::hash long_; //!< Long version of payment id (always decrypted)
|
|
} payment_id;
|
|
};
|
|
static_assert(
|
|
sizeof(output) == 8 + 32 + (8 * 3) + (4 * 2) + 32 + (8 * 2) + (32 * 3) + 7 + 1 + 32,
|
|
"padding in output"
|
|
);
|
|
|
|
//! Original db value, with no subaddress
|
|
struct spend
|
|
{
|
|
transaction_link link; //!< Orders and links `spend` to `output`.
|
|
crypto::key_image image; //!< Unique ID for the spend
|
|
// `link` and `image` must in this order for LMDB optimizations
|
|
output_id source; //!< The output being spent
|
|
std::uint64_t timestamp; //!< Timestamp of spend
|
|
std::uint64_t unlock_time;//!< Unlock time of spend
|
|
std::uint32_t mixin_count;//!< Ring-size of TX output
|
|
char reserved[3];
|
|
std::uint8_t length; //!< Length of `payment_id` field (0..32).
|
|
crypto::hash payment_id; //!< Unencrypted only, can't decrypt spend
|
|
};
|
|
static_assert(sizeof(spend) == 8 + 32 * 2 + 8 * 4 + 4 + 3 + 1 + 32, "padding in spend");
|
|
}
|
|
|
|
namespace v1
|
|
{
|
|
//! Second DB value, with no subaddress
|
|
struct output
|
|
{
|
|
transaction_link link; //! Orders and links `output` to `spend`s.
|
|
|
|
//! Data that a linked `spend` needs in some REST endpoints.
|
|
struct spend_meta_
|
|
{
|
|
output_id id; //!< Unique id for output within monero
|
|
// `link` and `id` must be in this order for LMDB optimizations
|
|
std::uint64_t amount;
|
|
std::uint32_t mixin_count;//!< Ring-size of TX
|
|
std::uint32_t index; //!< Offset within a tx
|
|
crypto::public_key tx_public;
|
|
} spend_meta;
|
|
|
|
std::uint64_t timestamp;
|
|
std::uint64_t unlock_time; //!< Not always a timestamp; mirrors chain value.
|
|
crypto::hash tx_prefix_hash;
|
|
crypto::public_key pub; //!< One-time spendable public key.
|
|
rct::key ringct_mask; //!< Unencrypted CT mask
|
|
char reserved[7];
|
|
extra_and_length extra; //!< Extra info + length of payment id
|
|
union payment_id_
|
|
{
|
|
crypto::hash8 short_; //!< Decrypted short payment id
|
|
crypto::hash long_; //!< Long version of payment id (always decrypted)
|
|
} payment_id;
|
|
std::uint64_t fee; //!< Total fee for transaction
|
|
};
|
|
static_assert(
|
|
sizeof(output) == 8 + 32 + (8 * 3) + (4 * 2) + 32 + (8 * 2) + (32 * 3) + 7 + 1 + 32 + 8,
|
|
"padding in output"
|
|
);
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
//! Used for finding `account` instances by other indexes.
|
|
struct account_lookup
|
|
{
|
|
account_id id;
|
|
account_status status;
|
|
char reserved[3];
|
|
};
|
|
static_assert(sizeof(account_lookup) == 4 + 1 + 3, "padding in account_lookup");
|
|
|
|
//! Used for looking up accounts by their public address.
|
|
struct account_by_address
|
|
{
|
|
account_address address; //!< Must be first for LMDB optimizations
|
|
account_lookup lookup;
|
|
};
|
|
static_assert(sizeof(account_by_address) == 64 + 4 + 1 + 3, "padding in account_by_address");
|
|
|
|
constexpr const unsigned blocks_version = 0;
|
|
constexpr const unsigned by_address_version = 0;
|
|
constexpr const unsigned pows_version = 0;
|
|
|
|
template<typename T>
|
|
int less(epee::span<const std::uint8_t> left, epee::span<const std::uint8_t> right) noexcept
|
|
{
|
|
if (left.size() < sizeof(T))
|
|
{
|
|
assert(left.empty());
|
|
return -1;
|
|
}
|
|
if (right.size() < sizeof(T))
|
|
{
|
|
assert(right.empty());
|
|
return 1;
|
|
}
|
|
|
|
T left_val;
|
|
T right_val;
|
|
std::memcpy(std::addressof(left_val), left.data(), sizeof(T));
|
|
std::memcpy(std::addressof(right_val), right.data(), sizeof(T));
|
|
|
|
return (left_val < right_val) ? -1 : int(right_val < left_val);
|
|
}
|
|
|
|
int compare_32bytes(epee::span<const std::uint8_t> left, epee::span<const std::uint8_t> right) noexcept
|
|
{
|
|
if (left.size() < 32)
|
|
{
|
|
assert(left.empty());
|
|
return -1;
|
|
}
|
|
if (right.size() < 32)
|
|
{
|
|
assert(right.empty());
|
|
return 1;
|
|
}
|
|
|
|
return std::memcmp(left.data(), right.data(), 32);
|
|
}
|
|
|
|
int output_compare(MDB_val const* left, MDB_val const* right) noexcept
|
|
{
|
|
if (left == nullptr || right == nullptr)
|
|
{
|
|
assert("MDB_val nullptr" == 0);
|
|
return -1;
|
|
}
|
|
|
|
auto left_bytes = lmdb::to_byte_span(*left);
|
|
auto right_bytes = lmdb::to_byte_span(*right);
|
|
|
|
int diff = less<lmdb::native_type<block_id>>(left_bytes, right_bytes);
|
|
if (diff)
|
|
return diff;
|
|
|
|
left_bytes.remove_prefix(sizeof(block_id));
|
|
right_bytes.remove_prefix(sizeof(block_id));
|
|
|
|
static_assert(sizeof(crypto::hash) == 32, "bad memcmp below");
|
|
diff = compare_32bytes(left_bytes, right_bytes);
|
|
if (diff)
|
|
return diff;
|
|
|
|
left_bytes.remove_prefix(sizeof(crypto::hash));
|
|
right_bytes.remove_prefix(sizeof(crypto::hash));
|
|
return less<output_id>(left_bytes, right_bytes);
|
|
}
|
|
|
|
int spend_compare(MDB_val const* left, MDB_val const* right) noexcept
|
|
{
|
|
if (left == nullptr || right == nullptr)
|
|
{
|
|
assert("MDB_val nullptr" == 0);
|
|
return -1;
|
|
}
|
|
|
|
auto left_bytes = lmdb::to_byte_span(*left);
|
|
auto right_bytes = lmdb::to_byte_span(*right);
|
|
|
|
int diff = less<lmdb::native_type<block_id>>(left_bytes, right_bytes);
|
|
if (diff)
|
|
return diff;
|
|
|
|
left_bytes.remove_prefix(sizeof(block_id));
|
|
right_bytes.remove_prefix(sizeof(block_id));
|
|
|
|
static_assert(sizeof(crypto::hash) == 32, "bad memcmp below");
|
|
diff = compare_32bytes(left_bytes, right_bytes);
|
|
if (diff)
|
|
return diff;
|
|
|
|
left_bytes.remove_prefix(sizeof(crypto::hash));
|
|
right_bytes.remove_prefix(sizeof(crypto::hash));
|
|
|
|
static_assert(sizeof(crypto::key_image) == 32, "bad memcmp below");
|
|
diff = compare_32bytes(left_bytes, right_bytes);
|
|
if (diff)
|
|
return diff;
|
|
|
|
left_bytes.remove_prefix(sizeof(crypto::key_image));
|
|
right_bytes.remove_prefix(sizeof(crypto::key_image));
|
|
return less<output_id>(left_bytes, right_bytes);
|
|
}
|
|
|
|
constexpr const lmdb::basic_table<unsigned, block_info> blocks{
|
|
"blocks_by_id", (MDB_CREATE | MDB_DUPSORT), MONERO_SORT_BY(block_info, id)
|
|
};
|
|
constexpr const lmdb::basic_table<unsigned, block_pow> pows{
|
|
"pow_by_id", (MDB_CREATE | MDB_DUPSORT), MONERO_SORT_BY(block_pow, id)
|
|
};
|
|
constexpr const lmdb::basic_table<account_status, account> accounts{
|
|
"accounts_by_status,id", (MDB_CREATE | MDB_DUPSORT), MONERO_SORT_BY(account, id)
|
|
};
|
|
constexpr const lmdb::basic_table<unsigned, account_by_address> accounts_by_address(
|
|
"accounts_by_address", (MDB_CREATE | MDB_DUPSORT), MONERO_COMPARE(account_by_address, address.view_public)
|
|
);
|
|
constexpr const lmdb::basic_table<block_id, account_lookup> accounts_by_height(
|
|
"accounts_by_height,id", (MDB_CREATE | MDB_DUPSORT), MONERO_SORT_BY(account_lookup, id)
|
|
);
|
|
constexpr const lmdb::basic_table<account_id, v0::output> outputs_v0{
|
|
"outputs_by_account_id,block_id,tx_hash,output_id", MDB_DUPSORT, &output_compare
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, v1::output> outputs_v1{
|
|
"outputs_v1_by_account_id,block_id,tx_hash,output_id", MDB_DUPSORT, &output_compare
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, output> outputs{
|
|
"outputs_v2_by_account_id,block_id,tx_hash,output_id", (MDB_CREATE | MDB_DUPSORT), &output_compare
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, v0::spend> spends_v0{
|
|
"spends_by_account_id,block_id,tx_hash,image", MDB_DUPSORT, &spend_compare
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, spend> spends{
|
|
"spends_v1_by_account_id,block_id,tx_hash,image", (MDB_CREATE | MDB_DUPSORT), &spend_compare
|
|
};
|
|
constexpr const lmdb::basic_table<output_id, db::key_image> images{
|
|
"key_images_by_output_id,image", (MDB_CREATE | MDB_DUPSORT), MONERO_COMPARE(db::key_image, value)
|
|
};
|
|
constexpr const lmdb::basic_table<request, request_info> requests{
|
|
"requests_by_type,address", (MDB_CREATE | MDB_DUPSORT), MONERO_COMPARE(request_info, address.spend_public)
|
|
};
|
|
constexpr const lmdb::msgpack_table<webhook_key, webhook_dupsort, webhook_data> webhooks{
|
|
"webhooks_by_account_id,payment_id", (MDB_CREATE | MDB_DUPSORT), &lmdb::less<db::webhook_dupsort>
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, webhook_event> events_by_account_id{
|
|
"webhook_events_by_account_id,type,block_id,tx_hash,output_id,payment_id,event_id", (MDB_CREATE | MDB_DUPSORT), &lmdb::less<webhook_event>
|
|
};
|
|
constexpr const lmdb::msgpack_table<account_id, major_index, index_ranges> subaddress_ranges{
|
|
"subaddress_ranges_by_account_id,major_index", (MDB_CREATE | MDB_DUPSORT), &lmdb::less<db::major_index>
|
|
};
|
|
constexpr const lmdb::basic_table<account_id, subaddress_map> subaddress_indexes{
|
|
"subaddress_indexes_by_account_id,public_key", (MDB_CREATE | MDB_DUPSORT), MONERO_COMPARE(subaddress_map, subaddress)
|
|
};
|
|
|
|
template<typename D>
|
|
expect<void> check_cursor(MDB_txn& txn, MDB_dbi tbl, std::unique_ptr<MDB_cursor, D>& cur) noexcept
|
|
{
|
|
if (cur)
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_renew(&txn, cur.get()));
|
|
}
|
|
else
|
|
{
|
|
auto new_cur = lmdb::open_cursor<D>(txn, tbl);
|
|
if (!new_cur)
|
|
return new_cur.error();
|
|
cur = std::move(*new_cur);
|
|
}
|
|
return success();
|
|
}
|
|
|
|
template<typename K, typename V>
|
|
expect<void> bulk_insert(MDB_cursor& cur, K const& key, epee::span<V> values, unsigned flags = MDB_NODUPDATA) noexcept
|
|
{
|
|
while (!values.empty())
|
|
{
|
|
void const* const data = reinterpret_cast<void const*>(values.data());
|
|
MDB_val key_bytes = lmdb::to_val(key);
|
|
MDB_val value_bytes[2] = {
|
|
MDB_val{sizeof(V), const_cast<void*>(data)}, MDB_val{values.size(), nullptr}
|
|
};
|
|
|
|
int err = mdb_cursor_put(
|
|
&cur, &key_bytes, value_bytes, (flags | MDB_MULTIPLE)
|
|
);
|
|
if (err && err != MDB_KEYEXIST)
|
|
return {lmdb::error(err)};
|
|
|
|
values.remove_prefix(value_bytes[1].mv_size + (err == MDB_KEYEXIST ? 1 : 0));
|
|
}
|
|
return success();
|
|
}
|
|
|
|
//! Convert table to new format, then delete old table
|
|
template<typename X, typename Y>
|
|
expect<void> convert_table(MDB_txn& txn, MDB_dbi old, MDB_dbi current)
|
|
{
|
|
MINFO("DB update: " + boost::core::demangle(typeid(X).name()) + " to " + boost::core::demangle(typeid(Y).name()));
|
|
|
|
cursor::outputs old_cur;
|
|
cursor::outputs current_cur;
|
|
MONERO_CHECK(check_cursor(txn, old, old_cur));
|
|
MONERO_CHECK(check_cursor(txn, current, current_cur));
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
int err = mdb_cursor_get(old_cur.get(), &key, &value, MDB_FIRST);
|
|
for (;;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
{
|
|
// Remove old table entirely
|
|
MONERO_LMDB_CHECK(mdb_drop(&txn, old, 1));
|
|
return success();
|
|
}
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
static_assert(sizeof(Y) >= sizeof(X), "unexpected sizeof");
|
|
if (sizeof(X) != value.mv_size)
|
|
return {lmdb::error(MDB_CORRUPTED)};
|
|
|
|
Y transition{};
|
|
std::memcpy(std::addressof(transition), value.mv_data, value.mv_size);
|
|
|
|
value = lmdb::to_val(transition);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(current_cur.get(), &key, &value, 0));
|
|
err = mdb_cursor_get(old_cur.get(), &key, &value, MDB_NEXT);
|
|
}
|
|
}
|
|
|
|
//! \return Current block hash at `id` using `cur`.
|
|
expect<crypto::hash> do_get_block_hash(MDB_cursor& cur, block_id id) noexcept
|
|
{
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value = lmdb::to_val(id);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_GET_BOTH));
|
|
return blocks.get_value<MONERO_FIELD(block_info, hash)>(value);
|
|
}
|
|
|
|
void check_blockchain(MDB_txn& txn, MDB_dbi tbl)
|
|
{
|
|
cursor::blocks cur = MONERO_UNWRAP(lmdb::open_cursor<cursor::close_blocks>(txn, tbl));
|
|
|
|
std::map<std::uint64_t, crypto::hash> const& points =
|
|
storage::get_checkpoints().get_points();
|
|
|
|
if (points.empty() || points.begin()->first != 0)
|
|
MONERO_THROW(lws::error::bad_blockchain, "Checkpoints are empty/expected genesis hash");
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
int err = mdb_cursor_get(cur.get(), &key, nullptr, MDB_SET);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
MONERO_THROW(lmdb::error(err), "Unable to retrieve blockchain hashes");
|
|
|
|
// new database
|
|
block_info checkpoint{
|
|
block_id(points.begin()->first), points.begin()->second
|
|
};
|
|
|
|
MDB_val value = lmdb::to_val(checkpoint);
|
|
err = mdb_cursor_put(cur.get(), &key, &value, MDB_NODUPDATA);
|
|
if (err)
|
|
MONERO_THROW(lmdb::error(err), "Unable to add hash to local blockchain");
|
|
|
|
if (1 < points.size())
|
|
{
|
|
checkpoint = block_info{
|
|
block_id(points.rbegin()->first), points.rbegin()->second
|
|
};
|
|
|
|
value = lmdb::to_val(checkpoint);
|
|
err = mdb_cursor_put(cur.get(), &key, &value, MDB_NODUPDATA);
|
|
if (err)
|
|
MONERO_THROW(lmdb::error(err), "Unable to add hash to local blockchain");
|
|
}
|
|
}
|
|
else // inspect existing database
|
|
{
|
|
///
|
|
/// TODO Trim blockchain after a checkpoint has been reached
|
|
///
|
|
const crypto::hash genesis = MONERO_UNWRAP(do_get_block_hash(*cur, block_id(0)));
|
|
if (genesis != points.begin()->second)
|
|
{
|
|
MONERO_THROW(
|
|
lws::error::bad_blockchain, "Genesis hash mismatch"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void check_pow(MDB_txn& txn, MDB_dbi tbl)
|
|
{
|
|
cursor::pow cur = MONERO_UNWRAP(lmdb::open_cursor<cursor::close_pow>(txn, tbl));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
int err = mdb_cursor_get(cur.get(), &key, nullptr, MDB_SET);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
MONERO_THROW(lmdb::error(err), "Unable to retrieve blockchain hashes");
|
|
|
|
// new database
|
|
block_pow checkpoint{block_id(0), 0u, block_difficulty{0u, 1u}};
|
|
MDB_val value = lmdb::to_val(checkpoint);
|
|
err = mdb_cursor_put(cur.get(), &key, &value, MDB_NODUPDATA);
|
|
if (err)
|
|
MONERO_THROW(lmdb::error(err), "Unable to add hash to local blockchain");
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
expect<void> get_blocks_tail(T& out, MDB_cursor& cur, MDB_val value, std::size_t max_internal)
|
|
{
|
|
for (unsigned i = 0; i < 10; ++i)
|
|
{
|
|
expect<block_info> next = blocks.get_value<block_info>(value);
|
|
if (!next)
|
|
return next.error();
|
|
|
|
out.push_back(std::move(*next));
|
|
|
|
MDB_val key{};
|
|
const int err = mdb_cursor_get(&cur, &key, &value, MDB_PREV_DUP);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
if (out.back().id != block_id(0))
|
|
return {lws::error::bad_blockchain};
|
|
return success();
|
|
}
|
|
}
|
|
|
|
const auto add_block = [&cur, &out] (std::uint64_t id) -> expect<void>
|
|
{
|
|
expect<crypto::hash> next = do_get_block_hash(cur, block_id(id));
|
|
if (!next)
|
|
return next.error();
|
|
out.push_back(block_info{block_id(id), std::move(*next)});
|
|
return success();
|
|
};
|
|
|
|
const std::uint64_t checkpoint = lws::db::storage::get_checkpoints().get_max_height();
|
|
const std::uint64_t anchor = lmdb::to_native(out.back().id);
|
|
|
|
for (unsigned i = 1; i <= max_internal; ++i)
|
|
{
|
|
const std::uint64_t offset = 2 << i;
|
|
if (anchor < offset || anchor - offset < checkpoint)
|
|
break;
|
|
MONERO_CHECK(add_block(anchor - offset));
|
|
}
|
|
|
|
if (block_id(checkpoint) < out.back().id)
|
|
MONERO_CHECK(add_block(checkpoint));
|
|
if (out.back().id != block_id(0))
|
|
MONERO_CHECK(add_block(0));
|
|
|
|
return success();
|
|
}
|
|
|
|
template<typename T>
|
|
expect<T> get_blocks(MDB_cursor& cur, std::size_t max_internal)
|
|
{
|
|
T out{};
|
|
|
|
max_internal = std::min(std::size_t(64), max_internal);
|
|
out.reserve(12 + max_internal);
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value{};
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_LAST_DUP));
|
|
MONERO_CHECK(get_blocks_tail(out, cur, value, max_internal));
|
|
return out;
|
|
}
|
|
|
|
template<typename T>
|
|
expect<T> get_blocks_from_height(MDB_cursor& cur, std::size_t max_internal, block_id last_pow)
|
|
{
|
|
T out{};
|
|
|
|
max_internal = std::min(std::size_t(64), max_internal);
|
|
out.reserve(12 + max_internal);
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value = lmdb::to_val(last_pow);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_GET_BOTH));
|
|
MONERO_CHECK(get_blocks_tail(out, cur, value, max_internal));
|
|
return out;
|
|
}
|
|
|
|
template<typename T>
|
|
expect<T> get_pow_blocks(MDB_cursor& cur, std::size_t max_internal)
|
|
{
|
|
T out{};
|
|
|
|
max_internal = std::min(std::size_t(64), max_internal);
|
|
out.reserve(max_internal);
|
|
|
|
MDB_val key = lmdb::to_val(pows_version);
|
|
MDB_val value{};
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_LAST_DUP));
|
|
|
|
for (unsigned i = 0; i < max_internal; ++i)
|
|
{
|
|
expect<block_pow> next = pows.get_value<block_pow>(value);
|
|
if (!next)
|
|
return next.error();
|
|
|
|
out.push_back(std::move(*next));
|
|
|
|
MDB_val key{};
|
|
const int err = mdb_cursor_get(&cur, &key, &value, MDB_PREV_DUP);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
if (out.back().id != block_id(0))
|
|
return {lws::error::bad_blockchain};
|
|
return out;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
expect<account_id> find_last_id(MDB_cursor& cur) noexcept
|
|
{
|
|
account_id best = account_id(0);
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
|
|
int err = mdb_cursor_get(&cur, &key, &value, MDB_FIRST);
|
|
if (err == MDB_NOTFOUND)
|
|
return best;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
do
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&cur, &key, &value, MDB_LAST_DUP));
|
|
const expect<account_id> current =
|
|
accounts.get_value<MONERO_FIELD(account, id)>(value);
|
|
if (!current)
|
|
return current.error();
|
|
|
|
|
|
best = std::max(best, *current);
|
|
err = mdb_cursor_get(&cur, &key, &value, MDB_NEXT_NODUP);
|
|
if (err == MDB_NOTFOUND)
|
|
return best;
|
|
} while (err == 0);
|
|
return {lmdb::error(err)};
|
|
}
|
|
} // anonymous
|
|
|
|
struct storage_internal : lmdb::database
|
|
{
|
|
struct tables_
|
|
{
|
|
MDB_dbi blocks;
|
|
MDB_dbi pows;
|
|
MDB_dbi accounts;
|
|
MDB_dbi accounts_ba;
|
|
MDB_dbi accounts_bh;
|
|
MDB_dbi outputs;
|
|
MDB_dbi spends;
|
|
MDB_dbi images;
|
|
MDB_dbi requests;
|
|
MDB_dbi webhooks;
|
|
MDB_dbi events;
|
|
MDB_dbi subaddress_ranges;
|
|
MDB_dbi subaddress_indexes;
|
|
} tables;
|
|
|
|
const unsigned create_queue_max;
|
|
|
|
explicit storage_internal(lmdb::environment env, unsigned create_queue_max)
|
|
: lmdb::database(std::move(env)), tables{}, create_queue_max(create_queue_max)
|
|
{
|
|
lmdb::write_txn txn = this->create_write_txn().value();
|
|
assert(txn != nullptr);
|
|
|
|
tables.blocks = blocks.open(*txn).value();
|
|
tables.pows = pows.open(*txn).value();
|
|
tables.accounts = accounts.open(*txn).value();
|
|
tables.accounts_ba = accounts_by_address.open(*txn).value();
|
|
tables.accounts_bh = accounts_by_height.open(*txn).value();
|
|
tables.outputs = outputs.open(*txn).value();
|
|
tables.spends = spends.open(*txn).value();
|
|
tables.images = images.open(*txn).value();
|
|
tables.requests = requests.open(*txn).value();
|
|
tables.webhooks = webhooks.open(*txn).value();
|
|
tables.events = events_by_account_id.open(*txn).value();
|
|
tables.subaddress_ranges = subaddress_ranges.open(*txn).value();
|
|
tables.subaddress_indexes = subaddress_indexes.open(*txn).value();
|
|
|
|
const auto v0_outputs = outputs_v0.open(*txn);
|
|
if (v0_outputs)
|
|
MONERO_UNWRAP(convert_table<v0::output, output>(*txn, *v0_outputs, tables.outputs));
|
|
else if (v0_outputs != lmdb::error(MDB_NOTFOUND))
|
|
MONERO_THROW(v0_outputs.error(), "Error opening old outputs table");
|
|
|
|
const auto v1_outputs = outputs_v1.open(*txn);
|
|
if (v1_outputs)
|
|
MONERO_UNWRAP(convert_table<v1::output, output>(*txn, *v1_outputs, tables.outputs));
|
|
else if (v1_outputs != lmdb::error(MDB_NOTFOUND))
|
|
MONERO_THROW(v1_outputs.error(), "Error opening old outputs table");
|
|
|
|
const auto v0_spends = spends_v0.open(*txn);
|
|
if (v0_spends)
|
|
MONERO_UNWRAP(convert_table<v0::spend, spend>(*txn, *v0_spends, tables.spends));
|
|
else if (v0_spends != lmdb::error(MDB_NOTFOUND))
|
|
MONERO_THROW(v0_spends.error(), "Error opening old spends table");
|
|
|
|
check_blockchain(*txn, tables.blocks);
|
|
check_pow(*txn, tables.pows);
|
|
MONERO_UNWRAP(this->commit(std::move(txn)));
|
|
}
|
|
};
|
|
|
|
storage_reader::~storage_reader() noexcept
|
|
{}
|
|
|
|
expect<block_info> storage_reader::get_last_block() noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.blocks, curs.blocks_cur));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value{};
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(curs.blocks_cur.get(), &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(curs.blocks_cur.get(), &key, &value, MDB_LAST_DUP));
|
|
|
|
return blocks.get_value<block_info>(value);
|
|
}
|
|
|
|
expect<block_pow> storage_reader::get_last_pow_block() noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.pows, pow_cur));
|
|
|
|
MDB_val key = lmdb::to_val(pows_version);
|
|
MDB_val value{};
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_LAST_DUP));
|
|
|
|
return pows.get_value<block_pow>(value);
|
|
}
|
|
|
|
expect<crypto::hash> storage_reader::get_block_hash(const block_id height) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.blocks, curs.blocks_cur));
|
|
assert(curs.blocks_cur != nullptr);
|
|
|
|
return do_get_block_hash(*curs.blocks_cur, height);
|
|
}
|
|
|
|
expect<std::list<crypto::hash>> storage_reader::get_chain_sync()
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.blocks, curs.blocks_cur));
|
|
auto blocks = get_blocks<std::vector<block_info>>(*curs.blocks_cur, 64);
|
|
if (!blocks)
|
|
return blocks.error();
|
|
|
|
std::list<crypto::hash> out{};
|
|
for (block_info const& block : *blocks)
|
|
out.push_back(block.hash);
|
|
return out;
|
|
}
|
|
|
|
expect<std::list<crypto::hash>> storage_reader::get_pow_sync()
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.pows, pow_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.blocks, curs.blocks_cur));
|
|
|
|
MDB_val key = lmdb::to_val(pows_version);
|
|
MDB_val value{};
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_LAST_DUP));
|
|
|
|
const block_id pow_height =
|
|
MONERO_UNWRAP(pows.get_value<MONERO_FIELD(block_pow, id)>(value));
|
|
|
|
auto blocks = get_blocks_from_height<std::vector<block_info>>(*curs.blocks_cur, 64, pow_height);
|
|
if (!blocks)
|
|
return blocks.error();
|
|
|
|
std::list<crypto::hash> out{};
|
|
for (block_info const& block : *blocks)
|
|
out.push_back(block.hash);
|
|
return out;
|
|
}
|
|
|
|
expect<pow_window>storage_reader::get_pow_window(const db::block_id last)
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
pow_window out{};
|
|
if (last == block_id(0))
|
|
return out;
|
|
|
|
std::uint64_t next = 0;
|
|
static_assert(1 <= DIFFICULTY_BLOCKS_COUNT, "invalid DIFFICULTY_BLOCKS_COUNT value");
|
|
if (block_id(DIFFICULTY_BLOCKS_COUNT) < last)
|
|
next = std::uint64_t(last) - (DIFFICULTY_BLOCKS_COUNT - 1);
|
|
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.pows, pow_cur));
|
|
|
|
MDB_val key = lmdb::to_val(pows_version);
|
|
MDB_val value = lmdb::to_val(next);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
for (;;)
|
|
{
|
|
const auto insert = MONERO_UNWRAP(pows.get_value<block_pow>(value));
|
|
out.pow_timestamps.push_back(insert.timestamp);
|
|
out.cumulative_diffs.push_back(insert.cumulative_diff.get_difficulty());
|
|
|
|
++next;
|
|
if (next == std::uint64_t(last) + 1)
|
|
break;
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_NEXT_DUP));
|
|
}
|
|
|
|
if (last < db::block_id(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW))
|
|
return out;
|
|
|
|
next = std::uint64_t(last) - (BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW - 1);
|
|
key = lmdb::to_val(pows_version);
|
|
value = lmdb::to_val(next);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
for (;;)
|
|
{
|
|
out.median_timestamps.push_back(
|
|
MONERO_UNWRAP(pows.get_value<MONERO_FIELD(block_pow, timestamp)>(value))
|
|
);
|
|
|
|
++next;
|
|
if (next == std::uint64_t(last) + 1)
|
|
break;
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(pow_cur.get(), &key, &value, MDB_NEXT_DUP));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
expect<lmdb::key_stream<account_status, account, cursor::close_accounts>>
|
|
storage_reader::get_accounts(cursor::accounts cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr); // both are moved in pairs
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts, cur));
|
|
return accounts.get_key_stream(std::move(cur));
|
|
}
|
|
|
|
expect<lmdb::value_stream<account, cursor::close_accounts>>
|
|
storage_reader::get_accounts(account_status status, cursor::accounts cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr); // both are moved in pairs
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts, cur));
|
|
return accounts.get_value_stream(status, std::move(cur));
|
|
}
|
|
|
|
expect<account> storage_reader::get_account(const account_status status, const account_id id) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
cursor::accounts cur;
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts, cur));
|
|
assert(cur != nullptr);
|
|
|
|
MDB_val key = lmdb::to_val(status);
|
|
MDB_val value = lmdb::to_val(id);
|
|
const int err = mdb_cursor_get(cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
return {lws::error::account_not_found};
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
return accounts.get_value<account>(value);
|
|
}
|
|
|
|
expect<std::pair<account_status, account>>
|
|
storage_reader::get_account(account_address const& address) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts_ba, curs.accounts_ba_cur));
|
|
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(curs.accounts_ba_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
return {lws::error::account_not_found};
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
/* Database is only indexing by view public for possible CurveZMQ
|
|
authentication extensions. Verifying both public keys here - the function
|
|
takes the entire address as an argument. */
|
|
static_assert(offsetof(account_by_address, address) == 0, "unexpected field offset");
|
|
if (value.mv_size < sizeof(account_address) || std::memcmp(value.mv_data, &address, sizeof(account_address)) != 0)
|
|
return {lws::error::account_not_found};
|
|
|
|
const expect<account_lookup> lookup =
|
|
accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup)>(value);
|
|
if (!lookup)
|
|
return lookup.error();
|
|
|
|
const expect<account> user = get_account(lookup->status, lookup->id);
|
|
if (!user)
|
|
return user.error();
|
|
return {{lookup->status, *user}};
|
|
}
|
|
|
|
expect<lmdb::value_stream<output, cursor::close_outputs>>
|
|
storage_reader::get_outputs(account_id id, cursor::outputs cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.outputs, cur));
|
|
return outputs.get_value_stream(id, std::move(cur));
|
|
}
|
|
|
|
expect<lmdb::value_stream<spend, cursor::close_spends>>
|
|
storage_reader::get_spends(account_id id, cursor::spends cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.spends, cur));
|
|
return spends.get_value_stream(id, std::move(cur));
|
|
}
|
|
|
|
expect<lmdb::value_stream<db::key_image, cursor::close_images>>
|
|
storage_reader::get_images(output_id id, cursor::images cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.images, cur));
|
|
return images.get_value_stream(id, std::move(cur));
|
|
}
|
|
|
|
expect<lmdb::key_stream<request, request_info, cursor::close_requests>>
|
|
storage_reader::get_requests(cursor::requests cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.requests, cur));
|
|
return requests.get_key_stream(std::move(cur));
|
|
}
|
|
|
|
expect<request_info>
|
|
storage_reader::get_request(request type, account_address const& address, cursor::requests cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.requests, cur));
|
|
|
|
MDB_val key = lmdb::to_val(type);
|
|
MDB_val value = lmdb::to_val(address);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(cur.get(), &key, &value, MDB_GET_BOTH));
|
|
return requests.get_value<request_info>(value);
|
|
}
|
|
|
|
expect<std::vector<subaddress_dict>>
|
|
storage_reader::get_subaddresses(account_id id, cursor::subaddress_ranges cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.subaddress_ranges, cur));
|
|
|
|
MDB_val key = lmdb::to_val(id);
|
|
MDB_val value{};
|
|
std::vector<subaddress_dict> ranges{};
|
|
int err = mdb_cursor_get(cur.get(), &key, &value, MDB_SET_KEY);
|
|
if (!err)
|
|
{
|
|
std::size_t count = 0;
|
|
if (mdb_cursor_count(cur.get(), &count) == 0)
|
|
ranges.reserve(count);
|
|
}
|
|
for (;;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
ranges.push_back(MONERO_UNWRAP(subaddress_ranges.get_value(value)));
|
|
err = mdb_cursor_get(cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
}
|
|
return {std::move(ranges)};
|
|
}
|
|
|
|
expect<address_index>
|
|
storage_reader::find_subaddress(account_id id, crypto::public_key const& address, cursor::subaddress_indexes& cur) noexcept
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.subaddress_indexes, cur));
|
|
MDB_val key = lmdb::to_val(id);
|
|
MDB_val value = lmdb::to_val(address);
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(cur.get(), &key, &value, MDB_GET_BOTH));
|
|
return subaddress_indexes.get_value<MONERO_FIELD(subaddress_map, index)>(value);
|
|
}
|
|
|
|
expect<std::vector<webhook_value>>
|
|
storage_reader::find_webhook(webhook_key const& key, crypto::hash8 const& payment_id, cursor::webhooks cur)
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.webhooks, cur));
|
|
|
|
webhook_dupsort dup{};
|
|
|
|
static_assert(sizeof(dup.payment_id) == sizeof(payment_id), "bad memcpy");
|
|
std::memcpy(std::addressof(dup.payment_id), std::addressof(payment_id), sizeof(payment_id));
|
|
|
|
MDB_val lkey = lmdb::to_val(key);
|
|
MDB_val lvalue = lmdb::to_val(dup);
|
|
|
|
std::vector<webhook_value> result{};
|
|
int err = mdb_cursor_get(cur.get(), &lkey, &lvalue, MDB_GET_BOTH_RANGE);
|
|
for (;;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
if (webhooks.get_fixed_value<MONERO_FIELD(webhook_dupsort, payment_id)>(lvalue) != dup.payment_id)
|
|
break;
|
|
|
|
result.push_back(MONERO_UNWRAP(webhooks.get_value(lvalue)));
|
|
err = mdb_cursor_get(cur.get(), &lkey, &lvalue, MDB_NEXT_DUP);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
expect<std::vector<std::pair<webhook_key, std::vector<webhook_value>>>>
|
|
storage_reader::get_webhooks(cursor::webhooks cur)
|
|
{
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.webhooks, cur));
|
|
|
|
std::vector<std::pair<webhook_key, std::vector<webhook_value>>> out;
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
int err = mdb_cursor_get(cur.get(), &key, &value, MDB_FIRST);
|
|
for (;/* every key */;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
return {std::move(out)};
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
out.emplace_back(MONERO_UNWRAP(webhooks.get_key(key)), std::vector<webhook_value>{});
|
|
|
|
for (; /* every dup key */ ;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break; // inner duplicate key loop
|
|
return {lmdb::error(err)};
|
|
}
|
|
out.back().second.push_back(MONERO_UNWRAP(webhooks.get_value(value)));
|
|
err = mdb_cursor_get(cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
}
|
|
err = mdb_cursor_get(cur.get(), &key, &value, MDB_NEXT);
|
|
}
|
|
|
|
return {std::move(out)};
|
|
}
|
|
|
|
namespace
|
|
{
|
|
//! `write_bytes` implementation will forward a third argument for `show_keys`.
|
|
template<typename T>
|
|
struct show_keys_wrapper
|
|
{
|
|
T value;
|
|
bool show_keys;
|
|
};
|
|
|
|
//! Filter that will instruct type to `show_keys` (or not).
|
|
struct toggle_key_output
|
|
{
|
|
const bool show_keys;
|
|
|
|
template<typename T>
|
|
show_keys_wrapper<T> operator()(T value) const noexcept
|
|
{
|
|
return {std::move(value), show_keys};
|
|
}
|
|
};
|
|
|
|
struct output_id_key
|
|
{
|
|
std::string operator()(const output_id id) const
|
|
{
|
|
return std::to_string(id.high) + ":" + std::to_string(id.low);
|
|
}
|
|
};
|
|
|
|
template<typename T>
|
|
void write_bytes(wire::json_writer& dest, show_keys_wrapper<T> self)
|
|
{
|
|
lws::db::write_bytes(dest, self.value, self.show_keys);
|
|
}
|
|
void write_bytes(wire::json_writer& dest, const account_lookup self)
|
|
{
|
|
wire::object(dest, WIRE_FIELD_COPY(id), WIRE_FIELD_COPY(status));
|
|
}
|
|
}
|
|
|
|
// accounts_by_height is output as a sorted array of objects
|
|
static void write_bytes(wire::json_writer& dest, std::pair<block_id, boost::iterator_range<lmdb::value_iterator<account_lookup>>> self)
|
|
{
|
|
wire::object(dest,
|
|
wire::field("scan_height", self.first),
|
|
wire::field("accounts", wire::array(std::move(self.second)))
|
|
);
|
|
}
|
|
|
|
static void write_bytes(wire::json_writer& dest, const std::pair<webhook_key, std::vector<webhook_value>>& self)
|
|
{
|
|
wire::object(dest,
|
|
wire::field("key", std::cref(self.first)),
|
|
wire::field("value", std::cref(self.second))
|
|
);
|
|
}
|
|
|
|
static void write_bytes(wire::json_writer& dest, const std::pair<lws::db::account_id, std::vector<db::subaddress_dict>>& self)
|
|
{
|
|
wire::object(dest,
|
|
wire::field("id", std::cref(self.first)),
|
|
wire::field("subaddress_indexes", std::cref(self.second))
|
|
);
|
|
}
|
|
|
|
expect<void> storage_reader::json_debug(std::ostream& out, bool show_keys)
|
|
{
|
|
using boost::adaptors::reverse;
|
|
using boost::adaptors::transform;
|
|
|
|
MONERO_PRECOND(txn != nullptr);
|
|
assert(db != nullptr);
|
|
|
|
const auto address_as_key = [](account_by_address const& src)
|
|
{
|
|
return std::make_pair(address_string(src.address), src.lookup);
|
|
};
|
|
|
|
cursor::pow pow_cur;
|
|
cursor::accounts accounts_cur;
|
|
cursor::outputs outputs_cur;
|
|
cursor::spends spends_cur;
|
|
cursor::images images_cur;
|
|
cursor::requests requests_cur;
|
|
cursor::webhooks webhooks_cur;
|
|
cursor::webhooks events_cur;
|
|
cursor::subaddress_ranges ranges_cur;
|
|
cursor::subaddress_indexes indexes_cur;
|
|
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.blocks, curs.blocks_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.pows, pow_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts_ba, curs.accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.accounts_bh, curs.accounts_bh_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.outputs, outputs_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.spends, spends_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.images, images_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.requests, requests_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.webhooks, webhooks_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.events, events_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.subaddress_ranges, ranges_cur));
|
|
MONERO_CHECK(check_cursor(*txn, db->tables.subaddress_indexes, indexes_cur));
|
|
|
|
auto blocks_partial =
|
|
get_blocks<boost::container::static_vector<block_info, 12>>(*curs.blocks_cur, 0);
|
|
if (!blocks_partial)
|
|
return blocks_partial.error();
|
|
|
|
auto pow_partial =
|
|
get_pow_blocks<boost::container::static_vector<block_pow, 12>>(*pow_cur, 12);
|
|
if (!pow_partial)
|
|
return pow_partial.error();
|
|
|
|
auto accounts_stream = accounts.get_key_stream(std::move(accounts_cur));
|
|
if (!accounts_stream)
|
|
return accounts_stream.error();
|
|
|
|
auto accounts_ba_stream = accounts_by_address.get_value_stream(
|
|
by_address_version, std::move(curs.accounts_ba_cur)
|
|
);
|
|
if (!accounts_ba_stream)
|
|
return accounts_ba_stream.error();
|
|
|
|
auto accounts_bh_stream = accounts_by_height.get_key_stream(
|
|
std::move(curs.accounts_bh_cur)
|
|
);
|
|
if (!accounts_bh_stream)
|
|
return accounts_bh_stream.error();
|
|
|
|
auto outputs_stream = outputs.get_key_stream(std::move(outputs_cur));
|
|
if (!outputs_stream)
|
|
return outputs_stream.error();
|
|
|
|
auto spends_stream = spends.get_key_stream(std::move(spends_cur));
|
|
if (!spends_stream)
|
|
return spends_stream.error();
|
|
|
|
auto images_stream = images.get_key_stream(std::move(images_cur));
|
|
if (!images_stream)
|
|
return images_stream.error();
|
|
|
|
auto requests_stream = requests.get_key_stream(std::move(requests_cur));
|
|
if (!requests_stream)
|
|
return requests_stream.error();
|
|
|
|
const auto ranges_data = subaddress_ranges.get_all(*ranges_cur);
|
|
if (!ranges_data)
|
|
return ranges_data.error();
|
|
|
|
auto indexes_stream = subaddress_indexes.get_key_stream(std::move(indexes_cur));
|
|
if (!indexes_stream)
|
|
return indexes_stream.error();
|
|
|
|
// This list should be smaller ... ?
|
|
const auto webhooks_data = webhooks.get_all(*webhooks_cur);
|
|
if (!webhooks_data)
|
|
return webhooks_data.error();
|
|
|
|
auto events_stream = events_by_account_id.get_key_stream(std::move(events_cur));
|
|
if (!events_stream)
|
|
return events_stream.error();
|
|
|
|
const wire::as_array_filter<toggle_key_output> toggle_keys_filter{{show_keys}};
|
|
wire::json_stream_writer json_stream{out};
|
|
wire::object(json_stream,
|
|
wire::field(blocks.name, wire::array(reverse(*blocks_partial))),
|
|
wire::field(pows.name, wire::array(reverse(*pow_partial))),
|
|
wire::field(accounts.name, wire::as_object(accounts_stream->make_range(), wire::enum_as_string, toggle_keys_filter)),
|
|
wire::field(accounts_by_address.name, wire::as_object(transform(accounts_ba_stream->make_range(), address_as_key))),
|
|
wire::field(accounts_by_height.name, wire::array(accounts_bh_stream->make_range())),
|
|
wire::field(outputs.name, wire::as_object(outputs_stream->make_range(), wire::as_integer, wire::as_array)),
|
|
wire::field(spends.name, wire::as_object(spends_stream->make_range(), wire::as_integer, wire::as_array)),
|
|
wire::field(images.name, wire::as_object(images_stream->make_range(), output_id_key{}, wire::as_array)),
|
|
wire::field(requests.name, wire::as_object(requests_stream->make_range(), wire::enum_as_string, toggle_keys_filter)),
|
|
wire::field(subaddress_ranges.name, std::cref(*ranges_data)),
|
|
wire::field(subaddress_indexes.name, wire::as_object(indexes_stream->make_range(), wire::as_integer, wire::as_array)),
|
|
wire::field(webhooks.name, std::cref(*webhooks_data)),
|
|
wire::field(events_by_account_id.name, wire::as_object(events_stream->make_range(), wire::as_integer, wire::as_array))
|
|
);
|
|
json_stream.finish();
|
|
|
|
curs.accounts_ba_cur = accounts_ba_stream->give_cursor();
|
|
curs.accounts_bh_cur = accounts_bh_stream->give_cursor();
|
|
|
|
if (!out.good())
|
|
return {std::io_errc::stream};
|
|
return success();
|
|
}
|
|
|
|
lmdb::suspended_txn storage_reader::finish_read() noexcept
|
|
{
|
|
if (txn != nullptr)
|
|
{
|
|
assert(db != nullptr);
|
|
auto suspended = db->reset_txn(std::move(txn));
|
|
if (suspended) // errors not currently logged
|
|
return {std::move(*suspended)};
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
cryptonote::checkpoints const& storage::get_checkpoints()
|
|
{
|
|
struct initializer
|
|
{
|
|
cryptonote::checkpoints data;
|
|
|
|
initializer()
|
|
: data()
|
|
{
|
|
data.init_default_checkpoints(lws::config::network);
|
|
|
|
std::string const* genesis_tx = nullptr;
|
|
std::uint32_t genesis_nonce = 0;
|
|
|
|
switch (lws::config::network)
|
|
{
|
|
case cryptonote::TESTNET:
|
|
genesis_tx = std::addressof(::config::testnet::GENESIS_TX);
|
|
genesis_nonce = ::config::testnet::GENESIS_NONCE;
|
|
break;
|
|
|
|
case cryptonote::STAGENET:
|
|
genesis_tx = std::addressof(::config::stagenet::GENESIS_TX);
|
|
genesis_nonce = ::config::stagenet::GENESIS_NONCE;
|
|
break;
|
|
|
|
case cryptonote::MAINNET:
|
|
genesis_tx = std::addressof(::config::GENESIS_TX);
|
|
genesis_nonce = ::config::GENESIS_NONCE;
|
|
break;
|
|
|
|
default:
|
|
MONERO_THROW(lws::error::bad_blockchain, "Unsupported net type");
|
|
}
|
|
cryptonote::block b;
|
|
cryptonote::generate_genesis_block(b, *genesis_tx, genesis_nonce);
|
|
crypto::hash block_hash = cryptonote::get_block_hash(b);
|
|
if (!data.add_checkpoint(0, epee::to_hex::string(epee::as_byte_span(block_hash))))
|
|
MONERO_THROW(lws::error::bad_blockchain, "Genesis tx and checkpoints file mismatch");
|
|
}
|
|
};
|
|
static const initializer instance;
|
|
return instance.data;
|
|
}
|
|
|
|
block_info storage::get_last_checkpoint()
|
|
{
|
|
const auto& checkpoints = get_checkpoints().get_points();
|
|
if (checkpoints.empty())
|
|
MONERO_THROW(error::bad_blockchain, "Checkpoints invalid");
|
|
|
|
const auto last = checkpoints.rbegin();
|
|
return block_info{block_id(last->first), last->second};
|
|
}
|
|
|
|
storage storage::open(const char* path, unsigned create_queue_max)
|
|
{
|
|
return {
|
|
std::make_shared<storage_internal>(
|
|
MONERO_UNWRAP(lmdb::open_environment(path, 20)), create_queue_max
|
|
)
|
|
};
|
|
}
|
|
|
|
storage::~storage() noexcept
|
|
{}
|
|
|
|
storage storage::clone() const noexcept
|
|
{
|
|
return storage{db};
|
|
}
|
|
|
|
expect<storage_reader> storage::start_read(lmdb::suspended_txn txn) const
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
|
|
expect<lmdb::read_txn> reader = db->create_read_txn(std::move(txn));
|
|
if (!reader)
|
|
return reader.error();
|
|
|
|
assert(*reader != nullptr);
|
|
return storage_reader{db, std::move(*reader)};
|
|
}
|
|
|
|
namespace // sub functions for `sync_chain(...)`
|
|
{
|
|
expect<void>
|
|
rollback_spends(account_id user, block_id height, MDB_cursor& spends_cur, MDB_cursor& images_cur) noexcept
|
|
{
|
|
MDB_val key = lmdb::to_val(user);
|
|
MDB_val value = lmdb::to_val(height);
|
|
|
|
const int err = mdb_cursor_get(&spends_cur, &key, &value, MDB_GET_BOTH_RANGE);
|
|
if (err == MDB_NOTFOUND)
|
|
return success();
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
for (;;)
|
|
{
|
|
const expect<output_id> out = spends.get_value<MONERO_FIELD(spend, source)>(value);
|
|
if (!out)
|
|
return out.error();
|
|
|
|
const expect<crypto::key_image> image =
|
|
spends.get_value<MONERO_FIELD(spend, image)>(value);
|
|
if (!image)
|
|
return image.error();
|
|
|
|
key = lmdb::to_val(*out);
|
|
value = lmdb::to_val(*image);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&images_cur, &key, &value, MDB_GET_BOTH));
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&images_cur, 0));
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&spends_cur, 0));
|
|
const int err = mdb_cursor_get(&spends_cur, &key, &value, MDB_NEXT_DUP);
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
}
|
|
return success();
|
|
}
|
|
|
|
expect<void>
|
|
rollback_outputs(account_id user, block_id height, MDB_cursor& outputs_cur) noexcept
|
|
{
|
|
MDB_val key = lmdb::to_val(user);
|
|
MDB_val value = lmdb::to_val(height);
|
|
|
|
const int err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_GET_BOTH_RANGE);
|
|
if (err == MDB_NOTFOUND)
|
|
return success();
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
for (;;)
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&outputs_cur, 0));
|
|
const int err = mdb_cursor_get(&outputs_cur, &key, &value, MDB_NEXT_DUP);
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
}
|
|
return success();
|
|
}
|
|
|
|
expect<void> rollback_accounts(storage_internal::tables_ const& tables, MDB_txn& txn, block_id height)
|
|
{
|
|
cursor::accounts_by_height accounts_bh_cur;
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts_bh, accounts_bh_cur));
|
|
|
|
MDB_val key = lmdb::to_val(height);
|
|
MDB_val value{};
|
|
const int err = mdb_cursor_get(accounts_bh_cur.get(), &key, &value, MDB_SET_RANGE);
|
|
if (err == MDB_NOTFOUND)
|
|
return success();
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
std::vector<account_lookup> new_by_heights{};
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::outputs outputs_cur;
|
|
cursor::spends spends_cur;
|
|
cursor::images images_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.outputs, outputs_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.spends, spends_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.images, images_cur));
|
|
|
|
const std::uint64_t new_height = std::uint64_t(std::max(height, block_id(1))) - 1;
|
|
|
|
// rollback accounts
|
|
for (;;)
|
|
{
|
|
const expect<account_lookup> lookup =
|
|
accounts_by_height.get_value<account_lookup>(value);
|
|
if (!lookup)
|
|
return lookup.error();
|
|
|
|
key = lmdb::to_val(lookup->status);
|
|
value = lmdb::to_val(lookup->id);
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
expect<account> user = accounts.get_value<account>(value);
|
|
if (!user)
|
|
return user.error();
|
|
|
|
user->scan_height = block_id(new_height);
|
|
user->start_height = std::min(user->scan_height, user->start_height);
|
|
|
|
value = lmdb::to_val(*user);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_cur.get(), &key, &value, MDB_CURRENT));
|
|
|
|
new_by_heights.push_back(account_lookup{user->id, lookup->status});
|
|
MONERO_CHECK(rollback_outputs(user->id, height, *outputs_cur));
|
|
MONERO_CHECK(rollback_spends(user->id, height, *spends_cur, *images_cur));
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(accounts_bh_cur.get(), 0));
|
|
int err = mdb_cursor_get(accounts_bh_cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
if (err == MDB_NOTFOUND)
|
|
{
|
|
err = mdb_cursor_get(accounts_bh_cur.get(), &key, &value, MDB_NEXT_NODUP);
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
}
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
return bulk_insert(*accounts_bh_cur, new_height, epee::to_span(new_by_heights));
|
|
}
|
|
|
|
expect<void> rollback_events(storage_internal::tables_ const& tables, MDB_txn& txn, const block_id height)
|
|
{
|
|
cursor::webhooks webhooks_cur;
|
|
cursor::events events_cur;
|
|
MONERO_CHECK(check_cursor(txn, tables.webhooks, webhooks_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.events, events_cur));
|
|
|
|
MDB_val key = lmdb::to_val(height);
|
|
MDB_val value{};
|
|
|
|
int err = mdb_cursor_get(events_cur.get(), &key, &value, MDB_LAST);
|
|
for ( ; /* every user */ ; )
|
|
{
|
|
for ( ; /* every event */ ;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
return success();
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
const webhook_event event =
|
|
MONERO_UNWRAP(events_by_account_id.get_value<webhook_event>(value));
|
|
|
|
if (event.link.tx.height < height)
|
|
break; // inner for loop
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(events_cur.get(), 0));
|
|
err = mdb_cursor_get(events_cur.get(), &key, &value, MDB_PREV);
|
|
}
|
|
err = mdb_cursor_get(events_cur.get(), &key, &value, MDB_PREV_NODUP);
|
|
}
|
|
return success();
|
|
}
|
|
|
|
expect<void> rollback_chain(storage_internal::tables_ const& tables, MDB_txn& txn, MDB_cursor& cur, block_id height)
|
|
{
|
|
MDB_val key;
|
|
MDB_val value;
|
|
|
|
// rollback chain
|
|
int err = 0;
|
|
do
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&cur, 0));
|
|
err = mdb_cursor_get(&cur, &key, &value, MDB_NEXT_DUP);
|
|
} while (err == 0);
|
|
|
|
// rollback pow
|
|
{
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(txn, tables.pows, pow_cur));
|
|
|
|
MDB_val key = lmdb::to_val(pows_version);
|
|
MDB_val value = lmdb::to_val(height);
|
|
int err = mdb_cursor_get(pow_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
for (;;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(pow_cur.get(), 0));
|
|
err = mdb_cursor_get(pow_cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
}
|
|
}
|
|
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
|
|
MONERO_CHECK(rollback_accounts(tables, txn, height));
|
|
return rollback_events(tables, txn, height);
|
|
}
|
|
|
|
template<typename T>
|
|
expect<void> append_block_hashes(MDB_cursor& cur, db::block_id first, T const& chain)
|
|
{
|
|
std::uint64_t height = std::uint64_t(first);
|
|
boost::container::static_vector<block_info, 25> hashes{};
|
|
static_assert(sizeof(hashes) <= 1024, "using more stack space than expected");
|
|
|
|
for (auto current = chain.begin() ;; ++current)
|
|
{
|
|
if (current == chain.end() || hashes.size() == hashes.capacity())
|
|
{
|
|
// always overwrite, for pow case (where pows is catching up to blocks)
|
|
MONERO_CHECK(bulk_insert(cur, blocks_version, epee::to_span(hashes), 0));
|
|
if (current == chain.end())
|
|
return success();
|
|
hashes.clear();
|
|
}
|
|
|
|
hashes.push_back(block_info{db::block_id(height), *current});
|
|
++height;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
expect<void> append_pow(MDB_cursor& cur, db::block_id first, T const& chain)
|
|
{
|
|
std::uint64_t height = std::uint64_t(first);
|
|
boost::container::static_vector<block_pow, 31> pows{};
|
|
static_assert(sizeof(pows) <= 1024, "using more stack space than expected");
|
|
|
|
for (auto current = chain.begin() ;; ++current)
|
|
{
|
|
if (current == chain.end() || pows.size() == pows.capacity())
|
|
{
|
|
MONERO_CHECK(bulk_insert(cur, pows_version, epee::to_span(pows)));
|
|
if (current == chain.end())
|
|
return success();
|
|
pows.clear();
|
|
}
|
|
|
|
pows.push_back(block_pow{db::block_id(height), current->timestamp, current->cumulative_diff});
|
|
++height;
|
|
}
|
|
}
|
|
|
|
} // anonymous
|
|
|
|
expect<void> storage::rollback(block_id height)
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
|
|
return db->try_write([this, height] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value = lmdb::to_val(height);
|
|
const int err = mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err == MDB_NOTFOUND)
|
|
return success();
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
return rollback_chain(this->db->tables, txn, *blocks_cur, height);
|
|
});
|
|
}
|
|
|
|
expect<void> storage::sync_chain(block_id height, epee::span<const crypto::hash> hashes)
|
|
{
|
|
MONERO_PRECOND(!hashes.empty());
|
|
MONERO_PRECOND(db != nullptr);
|
|
|
|
return db->try_write([this, height, hashes] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
|
|
expect<crypto::hash> hash = do_get_block_hash(*blocks_cur, height);
|
|
if (!hash)
|
|
return hash.error();
|
|
|
|
// the first entry should always match on in the DB
|
|
if (*hash != *(hashes.begin()))
|
|
return {lws::error::bad_blockchain};
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
|
|
std::uint64_t current = std::uint64_t(height) + 1;
|
|
auto first = hashes.begin();
|
|
auto chain = boost::make_iterator_range(++first, hashes.end());
|
|
for ( ; !chain.empty(); chain.advance_begin(1), ++current)
|
|
{
|
|
const int err = mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
hash = blocks.get_value<MONERO_FIELD(block_info, hash)>(value);
|
|
if (!hash)
|
|
return hash.error();
|
|
|
|
if (*hash != chain.front())
|
|
{
|
|
if (current <= get_checkpoints().get_max_height())
|
|
{
|
|
/* Either the daemon is performing an attack with a fake chain, or
|
|
the daemon is still syncing. */
|
|
MERROR("Attempting rollback past last checkpoint. Wait until daemon finishes syncing - otherwise daemon is performing an attack.");
|
|
return {lws::error::bad_blockchain};
|
|
}
|
|
|
|
MONERO_CHECK(rollback_chain(this->db->tables, txn, *blocks_cur, db::block_id(current)));
|
|
break;
|
|
}
|
|
}
|
|
return append_block_hashes(*blocks_cur, db::block_id(current), chain);
|
|
});
|
|
}
|
|
|
|
expect<void> storage::sync_pow(block_id height, epee::span<const crypto::hash> hashes, epee::span<const pow_sync> pow)
|
|
{
|
|
MONERO_PRECOND(!hashes.empty());
|
|
MONERO_PRECOND(hashes.size() == pow.size());
|
|
MONERO_PRECOND(db != nullptr);
|
|
|
|
return db->try_write([this, height, hashes, pow] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
|
|
expect<crypto::hash> hash = do_get_block_hash(*blocks_cur, height);
|
|
if (!hash)
|
|
return hash.error();
|
|
|
|
// the first entry should always match on in the DB
|
|
if (*hash != *(hashes.begin()))
|
|
return {lws::error::bad_blockchain};
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
|
|
std::uint64_t current = std::uint64_t(height) + 1;
|
|
auto first = hashes.begin();
|
|
auto chain = boost::make_iterator_range(++first, hashes.end());
|
|
const auto& checkpoints = get_checkpoints();
|
|
for ( ; !chain.empty(); chain.advance_begin(1), ++current)
|
|
{
|
|
// if while syncing from beginning, a checkpoint was missed
|
|
const auto checkpoint = checkpoints.get_points().find(current);
|
|
if (checkpoint != checkpoints.get_points().end() && checkpoint->second != chain.front())
|
|
{
|
|
MERROR("Missed a checkpoint during sync_pow");
|
|
return {error::bad_blockchain};
|
|
}
|
|
|
|
const int err = mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_NEXT_DUP);
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
auto full_value = blocks.get_value<block_info>(value);
|
|
if (!full_value)
|
|
return full_value.error();
|
|
if (full_value->id != block_id(current)) // hit a checkpoint or other block that is ahead of pow
|
|
break;
|
|
|
|
if (full_value->hash != chain.front())
|
|
{
|
|
if (current <= checkpoints.get_max_height())
|
|
{
|
|
MERROR("Attempting rollback past last checkpoint; invalid daemon chain response");
|
|
return {lws::error::bad_blockchain};
|
|
}
|
|
MONERO_CHECK(rollback_chain(this->db->tables, txn, *blocks_cur, db::block_id(current)));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// scan checkpoints, this is hardened mode!
|
|
{
|
|
std::uint64_t current_copy = current;
|
|
for (const auto& current_hash : chain)
|
|
{
|
|
// if while syncing from beginning, a checkpoint was missed
|
|
const auto checkpoint = checkpoints.get_points().find(current_copy);
|
|
if (checkpoint != checkpoints.get_points().end() && checkpoint->second != current_hash)
|
|
{
|
|
MERROR("Missed a checkpoint during sync_pow");
|
|
return {error::bad_blockchain};
|
|
}
|
|
++current_copy;
|
|
}
|
|
}
|
|
|
|
auto first_pow = pow.begin() + std::ptrdiff_t(chain.begin() - hashes.begin());
|
|
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.pows, pow_cur));
|
|
MONERO_CHECK(append_block_hashes(*blocks_cur, db::block_id(current), chain));
|
|
return append_pow(*pow_cur, db::block_id(current), boost::make_iterator_range(first_pow, pow.end()));
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
expect<db::account_time> get_account_time() noexcept
|
|
{
|
|
const auto time = std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::system_clock::now().time_since_epoch()
|
|
);
|
|
|
|
if (time.count() < 0)
|
|
return {lws::error::system_clock_invalid_range};
|
|
if (std::numeric_limits<lmdb::native_type<db::account_time>>::max() < time.count())
|
|
return {lws::error::system_clock_invalid_range};
|
|
return db::account_time(time.count());
|
|
}
|
|
}
|
|
|
|
expect<void> storage::update_access_time(account_address const& address) noexcept
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, &address] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
const expect<db::account_time> current_time = get_account_time();
|
|
if (!current_time)
|
|
return current_time.error();
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts accounts_ba_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(accounts_ba_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
|
|
if (err == MDB_NOTFOUND)
|
|
return {lws::error::account_not_found};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
const expect<account_lookup> lookup =
|
|
accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup)>(value);
|
|
if (!lookup)
|
|
return lookup.error();
|
|
|
|
key = lmdb::to_val(lookup->status);
|
|
value = lmdb::to_val(lookup->id);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
|
|
expect<account> user = accounts.get_value<account>(value);
|
|
if (!user)
|
|
return user.error();
|
|
|
|
user->access = *current_time;
|
|
value = lmdb::to_val(*user);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_cur.get(), &key, &value, MDB_CURRENT));
|
|
return success();
|
|
});
|
|
}
|
|
|
|
expect<std::vector<account_address>>
|
|
storage::change_status(account_status status , epee::span<const account_address> addresses)
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, status, addresses] (MDB_txn& txn) -> expect<std::vector<account_address>>
|
|
{
|
|
std::vector<account_address> changed{};
|
|
changed.reserve(addresses.size());
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts accounts_ba_cur;
|
|
cursor::accounts accounts_bh_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_bh, accounts_bh_cur));
|
|
|
|
for (account_address const& address : addresses)
|
|
{
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(accounts_ba_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
|
|
if (err == MDB_NOTFOUND)
|
|
continue;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
expect<account_by_address> by_address =
|
|
accounts_by_address.get_value<account_by_address>(value);
|
|
if (!by_address)
|
|
return by_address.error();
|
|
|
|
const account_status current = by_address->lookup.status;
|
|
if (current != status)
|
|
{
|
|
by_address->lookup.status = status;
|
|
|
|
value = lmdb::to_val(*by_address);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_ba_cur.get(), &key, &value, MDB_CURRENT));
|
|
|
|
key = lmdb::to_val(current);
|
|
value = lmdb::to_val(by_address->lookup.id);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
|
|
expect<account> user = accounts.get_value<account>(value);
|
|
if (!user)
|
|
return user.error();
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(accounts_cur.get(), 0));
|
|
|
|
key = lmdb::to_val(status);
|
|
value = lmdb::to_val(*user);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_cur.get(), &key, &value, MDB_NODUPDATA));
|
|
|
|
key = lmdb::to_val(user->scan_height);
|
|
value = lmdb::to_val(user->id);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_bh_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
|
|
value = lmdb::to_val(by_address->lookup);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_bh_cur.get(), &key, &value, MDB_CURRENT));
|
|
}
|
|
|
|
changed.push_back(address);
|
|
}
|
|
|
|
return changed;
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
expect<void> do_add_account(MDB_cursor& accounts_cur, MDB_cursor& accounts_ba_cur, MDB_cursor& accounts_bh_cur, account const& user) noexcept
|
|
{
|
|
{
|
|
crypto::secret_key copy{};
|
|
crypto::public_key verify{};
|
|
static_assert(sizeof(copy) == sizeof(user.key), "bad memcpy");
|
|
std::memcpy(
|
|
std::addressof(unwrap(copy)), std::addressof(user.key), sizeof(copy)
|
|
);
|
|
|
|
if (!crypto::secret_key_to_public_key(copy, verify))
|
|
return {lws::error::bad_view_key};
|
|
|
|
if (verify != user.address.view_public)
|
|
return {lws::error::bad_view_key};
|
|
}
|
|
|
|
const account_status status =
|
|
user.flags == account_flags::admin_account ?
|
|
account_status::hidden : account_status::active;
|
|
const account_by_address by_address{user.address, {user.id, status}};
|
|
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(by_address);
|
|
const int err = mdb_cursor_put(&accounts_ba_cur, &key, &value, MDB_NODUPDATA);
|
|
|
|
if (err == MDB_KEYEXIST)
|
|
return {lws::error::account_exists};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
key = lmdb::to_val(user.scan_height);
|
|
value = lmdb::to_val(by_address.lookup);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_put(&accounts_bh_cur, &key, &value, MDB_NODUPDATA)
|
|
);
|
|
|
|
key = lmdb::to_val(by_address.lookup.status);
|
|
value = lmdb::to_val(user);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_put(&accounts_cur, &key, &value, MDB_NODUPDATA)
|
|
);
|
|
return success();
|
|
}
|
|
} // anonymous
|
|
|
|
expect<void> storage::add_account(account_address const& address, crypto::secret_key const& key, const account_flags flags) noexcept
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, &address, &key, flags] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
const expect<db::account_time> current_time = get_account_time();
|
|
if (!current_time)
|
|
return current_time.error();
|
|
|
|
cursor::blocks blocks_cur;
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::accounts_by_height accounts_bh_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_bh, accounts_bh_cur));
|
|
|
|
const expect<account_id> last_id = find_last_id(*accounts_cur);
|
|
if (!last_id)
|
|
return last_id.error();
|
|
|
|
MDB_val keyv = lmdb::to_val(blocks_version);
|
|
MDB_val value{};
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &keyv, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &keyv, &value, MDB_LAST_DUP));
|
|
|
|
const expect<block_id> height =
|
|
blocks.get_value<MONERO_FIELD(block_info, id)>(value);
|
|
if (!height)
|
|
return height.error();
|
|
|
|
const account_id next_id = account_id(lmdb::to_native(*last_id) + 1);
|
|
account user{};
|
|
user.id = next_id;
|
|
user.address = address;
|
|
static_assert(sizeof(user.key) == sizeof(key), "bad memcpy");
|
|
std::memcpy(std::addressof(user.key), std::addressof(key), sizeof(key));
|
|
user.start_height = *height;
|
|
user.scan_height = *height;
|
|
user.access = *current_time;
|
|
user.creation = *current_time;
|
|
user.flags = flags;
|
|
|
|
return do_add_account(
|
|
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, user
|
|
);
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
//! \return Success, even if `address` was not found (designed for
|
|
expect<void>
|
|
change_height(MDB_cursor& accounts_cur, MDB_cursor& accounts_ba_cur, MDB_cursor& accounts_bh_cur, block_id height, account_address const& address)
|
|
{
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(&accounts_ba_cur, &key, &value, MDB_GET_BOTH);
|
|
if (err == MDB_NOTFOUND)
|
|
return {lws::error::account_not_found};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
const expect<account_lookup> lookup =
|
|
accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup)>(value);
|
|
if (!lookup)
|
|
return lookup.error();
|
|
|
|
key = lmdb::to_val(lookup->status);
|
|
value = lmdb::to_val(lookup->id);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_get(&accounts_cur, &key, &value, MDB_GET_BOTH)
|
|
);
|
|
|
|
expect<account> user = accounts.get_value<account>(value);
|
|
if (!user)
|
|
return user.error();
|
|
|
|
const block_id current_height = user->scan_height;
|
|
user->scan_height = std::min(height, user->scan_height);
|
|
user->start_height = std::min(height, user->start_height);
|
|
|
|
value = lmdb::to_val(*user);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_put(&accounts_cur, &key, &value, MDB_CURRENT)
|
|
);
|
|
|
|
key = lmdb::to_val(current_height);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_get(&accounts_bh_cur, &key, &value, MDB_GET_BOTH)
|
|
);
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&accounts_bh_cur, 0));
|
|
|
|
key = lmdb::to_val(height);
|
|
value = lmdb::to_val(*lookup);
|
|
MONERO_LMDB_CHECK(
|
|
mdb_cursor_put(&accounts_bh_cur, &key, &value, MDB_NODUPDATA)
|
|
);
|
|
|
|
return success();
|
|
}
|
|
}
|
|
|
|
expect<std::vector<account_address>>
|
|
storage::rescan(db::block_id height, epee::span<const account_address> addresses)
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, height, addresses] (MDB_txn& txn) -> expect<std::vector<account_address>>
|
|
{
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value{};
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_LAST_DUP));
|
|
|
|
const expect<block_id> current_height =
|
|
blocks.get_value<MONERO_FIELD(block_info, id)>(value);
|
|
if (!current_height)
|
|
return current_height.error();
|
|
if (*current_height < height)
|
|
return {error::bad_height};
|
|
}
|
|
|
|
std::vector<account_address> updated{};
|
|
updated.reserve(addresses.size());
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::accounts_by_height accounts_bh_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_bh, accounts_bh_cur));
|
|
|
|
for (account_address const& address : addresses)
|
|
{
|
|
const expect<void> changed = change_height(
|
|
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, height, address
|
|
);
|
|
if (changed)
|
|
updated.push_back(address);
|
|
else if (changed != lws::error::account_not_found)
|
|
return changed.error();
|
|
}
|
|
return updated;
|
|
});
|
|
}
|
|
|
|
expect<std::vector<webhook_new_account>> storage::creation_request(account_address const& address, crypto::secret_key const& key, account_flags flags) noexcept
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
|
|
if (!db->create_queue_max)
|
|
return {lws::error::create_queue_max};
|
|
|
|
return db->try_write([this, &address, &key, flags] (MDB_txn& txn) -> expect<std::vector<webhook_new_account>>
|
|
{
|
|
const expect<db::account_time> current_time = get_account_time();
|
|
if (!current_time)
|
|
return current_time.error();
|
|
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::blocks blocks_cur;
|
|
cursor::accounts requests_cur;
|
|
cursor::webhooks webhooks_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.requests, requests_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.webhooks, webhooks_cur));
|
|
|
|
MDB_val keyv = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
|
|
int err = mdb_cursor_get(accounts_ba_cur.get(), &keyv, &value, MDB_GET_BOTH);
|
|
if (err != MDB_NOTFOUND)
|
|
{
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
return {lws::error::account_exists};
|
|
}
|
|
|
|
const request req = request::create;
|
|
keyv = lmdb::to_val(req);
|
|
value = MDB_val{};
|
|
err = mdb_cursor_get(requests_cur.get(), &keyv, &value, MDB_SET);
|
|
if (!err)
|
|
{
|
|
mdb_size_t count = 0;
|
|
MONERO_LMDB_CHECK(mdb_cursor_count(requests_cur.get(), &count));
|
|
if (this->db->create_queue_max <= count)
|
|
return {lws::error::create_queue_max};
|
|
}
|
|
else if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
|
|
keyv = lmdb::to_val(blocks_version);
|
|
value = MDB_val{};
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &keyv, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &keyv, &value, MDB_LAST_DUP));
|
|
|
|
const expect<block_id> height =
|
|
blocks.get_value<MONERO_FIELD(block_info, id)>(value);
|
|
if (!height)
|
|
return height.error();
|
|
|
|
request_info info{};
|
|
info.address = address;
|
|
static_assert(sizeof(info.key) == sizeof(key), "bad memcpy");
|
|
std::memcpy(std::addressof(info.key), std::addressof(key), sizeof(key));
|
|
info.creation = *current_time;
|
|
info.start_height = *height;
|
|
info.creation_flags = flags;
|
|
|
|
keyv = lmdb::to_val(req);
|
|
value = lmdb::to_val(info);
|
|
|
|
err = mdb_cursor_put(requests_cur.get(), &keyv, &value, MDB_NODUPDATA);
|
|
if (err == MDB_KEYEXIST)
|
|
return {lws::error::duplicate_request};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
std::vector<webhook_new_account> hooks{};
|
|
webhook_key wkey{account_id::invalid, webhook_type::new_account};
|
|
keyv = lmdb::to_val(wkey);
|
|
err = mdb_cursor_get(webhooks_cur.get(), &keyv, &value, MDB_SET_KEY);
|
|
for (;;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
hooks.push_back(webhook_new_account{MONERO_UNWRAP(webhooks.get_value(value)), address});
|
|
err = mdb_cursor_get(webhooks_cur.get(), &keyv, &value, MDB_NEXT_DUP);
|
|
}
|
|
|
|
return hooks;
|
|
});
|
|
}
|
|
|
|
expect<void> storage::import_request(account_address const& address, block_id height) noexcept
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, &address, height] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
const expect<db::account_time> current_time = get_account_time();
|
|
if (!current_time)
|
|
return current_time.error();
|
|
|
|
cursor::blocks accounts_ba_cur;
|
|
cursor::requests requests_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.requests, requests_cur));
|
|
|
|
MDB_val key = lmdb::to_val(by_address_version);
|
|
MDB_val value = lmdb::to_val(address);
|
|
|
|
int err = mdb_cursor_get(accounts_ba_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err == MDB_NOTFOUND)
|
|
return {lws::error::account_not_found};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
request_info info{};
|
|
info.address = address;
|
|
info.start_height = height;
|
|
|
|
const request req = request::import_scan;
|
|
key = lmdb::to_val(req);
|
|
value = lmdb::to_val(info);
|
|
|
|
err = mdb_cursor_put(requests_cur.get(), &key, &value, MDB_NODUPDATA);
|
|
if (err == MDB_KEYEXIST)
|
|
return {lws::error::duplicate_request};
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
return success();
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
expect<std::vector<account_address>>
|
|
create_accounts(MDB_txn& txn, storage_internal::tables_ const& tables, epee::span<const account_address> addresses)
|
|
{
|
|
std::vector<account_address> stored{};
|
|
stored.reserve(addresses.size());
|
|
|
|
const expect<db::account_time> current_time = get_account_time();
|
|
if (!current_time)
|
|
return current_time.error();
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::accounts_by_height accounts_bh_cur;
|
|
cursor::requests requests_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts_bh, accounts_bh_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.requests, requests_cur));
|
|
|
|
expect<account_id> last_id = find_last_id(*accounts_cur);
|
|
if (!last_id)
|
|
return last_id.error();
|
|
|
|
const request req = request::create;
|
|
for (account_address const& address : addresses)
|
|
{
|
|
MDB_val keyv = lmdb::to_val(req);
|
|
MDB_val value = lmdb::to_val(address);
|
|
int err = mdb_cursor_get(requests_cur.get(), &keyv, &value, MDB_GET_BOTH);
|
|
if (err == MDB_NOTFOUND)
|
|
continue;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
const expect<db::request_info> info = requests.get_value<db::request_info>(value);
|
|
if (!info)
|
|
return info.error();
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(requests_cur.get(), 0));
|
|
|
|
const account_id next_id = account_id(lmdb::to_native(*last_id) + 1);
|
|
if (next_id == account_id::invalid)
|
|
return {lws::error::account_max};
|
|
|
|
account user{};
|
|
user.id = next_id;
|
|
user.address = address;
|
|
user.key = info->key;
|
|
user.start_height = info->start_height;
|
|
user.scan_height = info->start_height;
|
|
user.access = *current_time;
|
|
user.creation = info->creation;
|
|
user.flags = info->creation_flags;
|
|
|
|
const expect<void> added =
|
|
do_add_account(*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, user);
|
|
|
|
if (!added)
|
|
{
|
|
if (added == lws::error::account_exists || added == lws::error::bad_view_key)
|
|
continue;
|
|
return added.error();
|
|
}
|
|
|
|
*last_id = next_id;
|
|
stored.push_back(address);
|
|
}
|
|
return stored;
|
|
}
|
|
|
|
expect<std::vector<account_address>>
|
|
import_accounts(MDB_txn& txn, storage_internal::tables_ const& tables, epee::span<const account_address> addresses)
|
|
{
|
|
std::vector<account_address> updated{};
|
|
updated.reserve(addresses.size());
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts accounts_ba_cur;
|
|
cursor::accounts accounts_bh_cur;
|
|
cursor::requests requests_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.accounts_bh, accounts_bh_cur));
|
|
MONERO_CHECK(check_cursor(txn, tables.requests, requests_cur));
|
|
|
|
const request req = request::import_scan;
|
|
for (account_address const& address : addresses)
|
|
{
|
|
MDB_val key = lmdb::to_val(req);
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(requests_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err == MDB_NOTFOUND)
|
|
continue;
|
|
if (err)
|
|
return {lmdb::error(err)};
|
|
|
|
const expect<block_id> new_height =
|
|
requests.get_value<MONERO_FIELD(request_info, start_height)>(value);
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(requests_cur.get(), 0));
|
|
if (!new_height)
|
|
return new_height.error();
|
|
|
|
const expect<void> changed = change_height(
|
|
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, *new_height, address
|
|
);
|
|
if (changed)
|
|
updated.push_back(address);
|
|
else if (changed != lws::error::account_not_found)
|
|
return changed.error();
|
|
}
|
|
return updated;
|
|
}
|
|
} // anonymous
|
|
|
|
expect<std::vector<account_address>>
|
|
storage::accept_requests(request req, epee::span<const account_address> addresses)
|
|
{
|
|
if (addresses.empty())
|
|
return std::vector<account_address>{};
|
|
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, req, addresses] (MDB_txn& txn) -> expect<std::vector<account_address>>
|
|
{
|
|
switch (req)
|
|
{
|
|
case request::create:
|
|
return create_accounts(txn, this->db->tables, addresses);
|
|
case request::import_scan:
|
|
return import_accounts(txn, this->db->tables, addresses);
|
|
default:
|
|
break;
|
|
}
|
|
return {common_error::kInvalidArgument};
|
|
});
|
|
}
|
|
|
|
expect<std::vector<account_address>>
|
|
storage::reject_requests(request req, epee::span<const account_address> addresses)
|
|
{
|
|
if (addresses.empty())
|
|
return std::vector<account_address>{};
|
|
|
|
MONERO_PRECOND(db != nullptr);
|
|
return db->try_write([this, req, addresses] (MDB_txn& txn) -> expect<std::vector<account_address>>
|
|
{
|
|
std::vector<account_address> rejected{};
|
|
|
|
cursor::requests requests_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.requests, requests_cur));
|
|
|
|
MDB_val key = lmdb::to_val(req);
|
|
for (account_address const& address : addresses)
|
|
{
|
|
MDB_val value = lmdb::to_val(address);
|
|
const int err = mdb_cursor_get(requests_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err && err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
|
|
if (!err)
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(requests_cur.get(), 0));
|
|
rejected.push_back(address);
|
|
}
|
|
}
|
|
|
|
return rejected;
|
|
});
|
|
}
|
|
|
|
namespace
|
|
{
|
|
expect<void>
|
|
add_spends(MDB_cursor& spends_cur, MDB_cursor& images_cur, account_id user, epee::span<const spend> spends) noexcept
|
|
{
|
|
MONERO_CHECK(bulk_insert(spends_cur, user, spends));
|
|
for (auto const& entry : spends)
|
|
{
|
|
const db::key_image image{entry.image, entry.link};
|
|
|
|
MDB_val key = lmdb::to_val(entry.source);
|
|
MDB_val value = lmdb::to_val(image);
|
|
const int err = mdb_cursor_put(&images_cur, &key, &value, MDB_NODUPDATA);
|
|
if (err && err != MDB_KEYEXIST)
|
|
return {lmdb::error(err)};
|
|
}
|
|
return success();
|
|
}
|
|
|
|
expect<void> check_hooks(MDB_cursor& webhooks_cur, MDB_cursor& events_cur, const lws::account& user)
|
|
{
|
|
const account_id user_id = user.id();
|
|
const webhook_key hook_key{user_id, webhook_type::tx_confirmation};
|
|
|
|
// check payment_id == x (match specific) webhooks second
|
|
for (const output& out : user.outputs())
|
|
{
|
|
webhook_dupsort sorter{};
|
|
static_assert(sizeof(sorter.payment_id) == sizeof(out.payment_id.short_), "bad memcpy");
|
|
std::memcpy(
|
|
std::addressof(sorter.payment_id), std::addressof(out.payment_id.short_), sizeof(sorter.payment_id)
|
|
);
|
|
|
|
MDB_val key = lmdb::to_val(hook_key);
|
|
MDB_val value = lmdb::to_val(sorter);
|
|
int err = mdb_cursor_get(&webhooks_cur, &key, &value, MDB_GET_BOTH_RANGE);
|
|
|
|
for (; /* all user/payment_id==x entries */ ;)
|
|
{
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
break;
|
|
}
|
|
const webhook_dupsort db_sorter = MONERO_UNWRAP(webhooks.get_fixed_value<webhook_dupsort>(value));
|
|
if (db_sorter.payment_id != sorter.payment_id)
|
|
break;
|
|
|
|
const webhook_event event{
|
|
webhook_output{out.link, out.spend_meta.id}, db_sorter
|
|
};
|
|
|
|
MDB_val ekey = lmdb::to_val(user_id);
|
|
MDB_val evalue = lmdb::to_val(event);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(&events_cur, &ekey, &evalue, 0));
|
|
err = mdb_cursor_get(&webhooks_cur, &key, &value, MDB_NEXT_DUP);
|
|
}
|
|
}
|
|
return success();
|
|
}
|
|
|
|
expect<void>
|
|
add_ongoing_hooks(std::vector<webhook_tx_confirmation>& events, MDB_cursor& webhooks_cur, MDB_cursor& outputs_cur, MDB_cursor& events_cur, const account_id user, const block_id begin, const block_id end)
|
|
{
|
|
if (begin == end)
|
|
return success();
|
|
|
|
const webhook_key hook_key{user, webhook_type::tx_confirmation};
|
|
MDB_val key = lmdb::to_val(user);
|
|
MDB_val value{};
|
|
|
|
int err = mdb_cursor_get(&events_cur, &key, &value, MDB_SET_KEY);
|
|
for ( ; /* every ongoing event from this user */ ; )
|
|
{
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
return success();
|
|
}
|
|
|
|
const webhook_event event =
|
|
MONERO_UNWRAP(events_by_account_id.get_value<webhook_event>(value));
|
|
|
|
MDB_val rkey = lmdb::to_val(hook_key);
|
|
MDB_val rvalue = lmdb::to_val(event.link_webhook);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&webhooks_cur, &rkey, &rvalue, MDB_GET_BOTH));
|
|
|
|
MDB_val okey = lmdb::to_val(user);
|
|
MDB_val ovalue = lmdb::to_val(event.link);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(&outputs_cur, &okey, &ovalue, MDB_GET_BOTH));
|
|
|
|
events.push_back(
|
|
webhook_tx_confirmation{
|
|
MONERO_UNWRAP(webhooks.get_key(rkey)),
|
|
MONERO_UNWRAP(webhooks.get_value(rvalue)),
|
|
MONERO_UNWRAP(outputs.get_value<output>(ovalue))
|
|
}
|
|
);
|
|
|
|
const std::uint32_t requested_confirmations =
|
|
events.back().value.second.confirmations;
|
|
|
|
events.back().value.second.confirmations =
|
|
lmdb::to_native(begin) - lmdb::to_native(event.link.tx.height) + 1;
|
|
|
|
// copy next blocks from first
|
|
for (const auto block_num : boost::counting_range(lmdb::to_native(begin) + 1, lmdb::to_native(end)))
|
|
{
|
|
if (requested_confirmations <= events.back().value.second.confirmations)
|
|
break;
|
|
events.push_back(events.back());
|
|
++(events.back().value.second.confirmations);
|
|
}
|
|
if (requested_confirmations <= events.back().value.second.confirmations)
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(&events_cur, 0));
|
|
err = mdb_cursor_get(&events_cur, &key, &value, MDB_NEXT_DUP);
|
|
}
|
|
return success();
|
|
}
|
|
} // anonymous
|
|
|
|
expect<std::pair<std::size_t, std::vector<webhook_tx_confirmation>>> storage::update(block_id height, epee::span<const crypto::hash> chain, epee::span<const lws::account> users, epee::span<const pow_sync> pow)
|
|
{
|
|
if (users.empty() && chain.empty())
|
|
return {std::make_pair(0, std::vector<webhook_tx_confirmation>{})};
|
|
MONERO_PRECOND(!chain.empty());
|
|
MONERO_PRECOND(db != nullptr);
|
|
if (!pow.empty())
|
|
MONERO_PRECOND(chain.size() == pow.size());
|
|
|
|
return db->try_write([this, height, chain, users, pow] (MDB_txn& txn) -> expect<std::pair<std::size_t, std::vector<webhook_tx_confirmation>>>
|
|
{
|
|
epee::span<const crypto::hash> chain_copy{chain};
|
|
epee::span<const pow_sync> pow_copy{pow};
|
|
const std::uint64_t last_update =
|
|
lmdb::to_native(height) + chain.size() - 1;
|
|
const std::uint64_t first_new = lmdb::to_native(height) + 1;
|
|
|
|
// collect all .value() errors
|
|
std::pair<std::size_t, std::vector<webhook_tx_confirmation>> updated;
|
|
if (get_checkpoints().get_max_height() <= last_update)
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
cursor::pow pow_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.pows, pow_cur));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value;
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_SET));
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_LAST_DUP));
|
|
|
|
const block_info last_block = MONERO_UNWRAP(blocks.get_value<block_info>(value));
|
|
if (last_block.id < height)
|
|
return {lws::error::bad_blockchain};
|
|
|
|
const std::uint64_t last_same =
|
|
std::min(lmdb::to_native(last_block.id), last_update);
|
|
|
|
const std::uint64_t offset = last_same - lmdb::to_native(height);
|
|
if (MONERO_UNWRAP(do_get_block_hash(*blocks_cur, block_id(last_same))) != *(chain_copy.begin() + offset))
|
|
return {lws::error::blockchain_reorg};
|
|
|
|
chain_copy.remove_prefix(offset + 1);
|
|
MONERO_CHECK(
|
|
append_block_hashes(
|
|
*blocks_cur, block_id(lmdb::to_native(height) + offset + 1), chain_copy
|
|
)
|
|
);
|
|
|
|
if (!pow_copy.empty())
|
|
{
|
|
pow_copy.remove_prefix(offset + 1);
|
|
MONERO_CHECK(
|
|
append_pow(*pow_cur, block_id(lmdb::to_native(height) + offset + 1), pow_copy)
|
|
);
|
|
}
|
|
}
|
|
else // perform chain/pow hardening via checkpoints (if available)
|
|
{
|
|
cursor::blocks blocks_cur;
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.blocks, blocks_cur));
|
|
|
|
MDB_val key = lmdb::to_val(blocks_version);
|
|
MDB_val value = lmdb::to_val(last_update);
|
|
int err = mdb_cursor_get(blocks_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
|
|
// verify last block hash if available. If not availble, --untrusted-daemon was not used
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
}
|
|
else
|
|
{
|
|
const auto cur_block = blocks.get_value<block_info>(value);
|
|
if (!cur_block)
|
|
return cur_block.error();
|
|
// If a reorg past a checkpoint is being attempted
|
|
if (chain[chain.size() - 1] != cur_block->hash)
|
|
return {error::bad_blockchain};
|
|
|
|
}
|
|
}
|
|
|
|
cursor::accounts accounts_cur;
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::accounts_by_height accounts_bh_cur;
|
|
cursor::outputs outputs_cur;
|
|
cursor::spends spends_cur;
|
|
cursor::images images_cur;
|
|
cursor::webhooks webhooks_cur;
|
|
cursor::events events_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts, accounts_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_bh, accounts_bh_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.outputs, outputs_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.spends, spends_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.images, images_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.webhooks, webhooks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.events, events_cur));
|
|
|
|
// for bulk inserts
|
|
boost::container::static_vector<account_lookup, 127> heights{};
|
|
static_assert(sizeof(heights) <= 1024, "stack vector is large");
|
|
|
|
for (auto user = users.begin() ;; ++user)
|
|
{
|
|
if (heights.size() == heights.capacity() || user == users.end())
|
|
{
|
|
// bulk update account height index
|
|
MONERO_CHECK(
|
|
bulk_insert(*accounts_bh_cur, last_update, epee::to_span(heights))
|
|
);
|
|
if (user == users.end())
|
|
break;
|
|
heights.clear();
|
|
}
|
|
|
|
// faster to assume that account is still active
|
|
account_status status_key = account_status::active;
|
|
const account_id user_id = user->id();
|
|
MDB_val key = lmdb::to_val(status_key);
|
|
MDB_val value = lmdb::to_val(user_id);
|
|
int err = mdb_cursor_get(accounts_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
if (accounts_ba_cur == nullptr)
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
|
|
MDB_val temp_key = lmdb::to_val(by_address_version);
|
|
MDB_val temp_value = lmdb::to_val(user->db_address());
|
|
err = mdb_cursor_get(accounts_ba_cur.get(), &temp_key, &temp_value, MDB_GET_BOTH);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
continue; // to next account
|
|
}
|
|
|
|
status_key =
|
|
accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup)>(temp_value).value().status;
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
}
|
|
expect<account> existing = accounts.get_value<account>(value);
|
|
if (!existing || existing->scan_height != user->scan_height())
|
|
continue; // to next account
|
|
|
|
const block_id existing_height = existing->scan_height;
|
|
|
|
existing->scan_height = block_id(last_update);
|
|
value = lmdb::to_val(*existing);
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(accounts_cur.get(), &key, &value, MDB_CURRENT));
|
|
|
|
heights.push_back(account_lookup{user->id(), status_key});
|
|
|
|
key = lmdb::to_val(existing_height);
|
|
value = lmdb::to_val(user_id);
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_bh_cur.get(), &key, &value, MDB_GET_BOTH));
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(accounts_bh_cur.get(), 0));
|
|
|
|
MONERO_CHECK(bulk_insert(*outputs_cur, user->id(), epee::to_span(user->outputs())));
|
|
MONERO_CHECK(add_spends(*spends_cur, *images_cur, user->id(), epee::to_span(user->spends())));
|
|
|
|
MONERO_CHECK(check_hooks(*webhooks_cur, *events_cur, *user));
|
|
MONERO_CHECK(
|
|
add_ongoing_hooks(
|
|
updated.second, *webhooks_cur, *outputs_cur, *events_cur, user->id(), block_id(first_new), block_id(last_update + 1)
|
|
)
|
|
);
|
|
|
|
++updated.first;
|
|
} // ... for every account being updated ...
|
|
return {std::move(updated)};
|
|
});
|
|
}
|
|
|
|
expect<std::vector<subaddress_dict>>
|
|
storage::upsert_subaddresses(const account_id id, const account_address& address, const crypto::secret_key& view_key, std::vector<subaddress_dict> subaddrs, const std::uint32_t max_subaddr)
|
|
{
|
|
MONERO_PRECOND(db != nullptr);
|
|
std::sort(subaddrs.begin(), subaddrs.end());
|
|
|
|
return db->try_write([this, id, &address, &view_key, &subaddrs, max_subaddr] (MDB_txn& txn) -> expect<std::vector<subaddress_dict>>
|
|
{
|
|
std::size_t subaddr_count = 0;
|
|
std::vector<subaddress_dict> out{};
|
|
index_ranges new_dict{};
|
|
const auto add_out = [&out] (major_index major, index_range minor)
|
|
{
|
|
if (out.empty() || out.back().first != major)
|
|
out.emplace_back(major, index_ranges{std::vector<index_range>{minor}});
|
|
else
|
|
out.back().second.get_container().push_back(minor);
|
|
};
|
|
|
|
const auto check_max_range = [&subaddr_count, max_subaddr] (const index_range& range) -> bool
|
|
{
|
|
const auto more = std::uint32_t(range[1]) - std::uint32_t(range[0]);
|
|
if (max_subaddr - subaddr_count <= more)
|
|
return false;
|
|
subaddr_count += more + 1;
|
|
return true;
|
|
};
|
|
const auto check_max_ranges = [&check_max_range] (const index_ranges& ranges) -> bool
|
|
{
|
|
for (const auto& range : ranges.get_container())
|
|
{
|
|
if (!check_max_range(range))
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
cursor::subaddress_ranges ranges_cur;
|
|
cursor::subaddress_indexes indexes_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.subaddress_ranges, ranges_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.subaddress_indexes, indexes_cur));
|
|
|
|
MDB_val key = lmdb::to_val(id);
|
|
MDB_val value{};
|
|
int err = mdb_cursor_get(indexes_cur.get(), &key, &value, MDB_SET);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
}
|
|
else
|
|
{
|
|
MONERO_LMDB_CHECK(mdb_cursor_count(indexes_cur.get(), &subaddr_count));
|
|
if (max_subaddr < subaddr_count)
|
|
return {error::max_subaddresses};
|
|
}
|
|
|
|
for (auto& major_entry : subaddrs)
|
|
{
|
|
new_dict.get_container().clear();
|
|
if (!check_subaddress_dict(major_entry))
|
|
{
|
|
MERROR("Invalid subaddress_dict given to storage::upsert_subaddrs");
|
|
return {wire::error::schema::array};
|
|
}
|
|
|
|
value = lmdb::to_val(major_entry.first);
|
|
err = mdb_cursor_get(ranges_cur.get(), &key, &value, MDB_GET_BOTH);
|
|
if (err)
|
|
{
|
|
if (err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
if (!check_max_ranges(major_entry.second))
|
|
return {error::max_subaddresses};
|
|
out.push_back(major_entry);
|
|
new_dict = std::move(major_entry.second);
|
|
}
|
|
else // merge new minor index ranges with old
|
|
{
|
|
auto old_dict = subaddress_ranges.get_value(value);
|
|
if (!old_dict)
|
|
return old_dict.error();
|
|
|
|
auto& old_range = old_dict->second.get_container();
|
|
const auto& new_range = major_entry.second.get_container();
|
|
|
|
auto old_loc = old_range.begin();
|
|
auto new_loc = new_range.begin();
|
|
for ( ; old_loc != old_range.end() && new_loc != new_range.end(); )
|
|
{
|
|
if (std::uint64_t(new_loc->at(1)) + 1 < std::uint32_t(old_loc->at(0)))
|
|
{ // new has no overlap with existing
|
|
if (!check_max_range(*new_loc))
|
|
return {error::max_subaddresses};
|
|
|
|
new_dict.get_container().push_back(*new_loc);
|
|
add_out(major_entry.first, *new_loc);
|
|
++new_loc;
|
|
}
|
|
else if (std::uint64_t(old_loc->at(1)) + 1 < std::uint32_t(new_loc->at(0)))
|
|
{ // existing has no overlap with new
|
|
new_dict.get_container().push_back(*old_loc);
|
|
++old_loc;
|
|
}
|
|
else if (old_loc->at(0) <= new_loc->at(0) && new_loc->at(1) <= old_loc->at(1))
|
|
{ // new is completely within existing
|
|
++new_loc;
|
|
}
|
|
else // new overlap at beginning, end, or both
|
|
{
|
|
if (new_loc->at(0) < old_loc->at(0))
|
|
{ // overlap at beginning
|
|
const index_range new_range{new_loc->at(0), minor_index(std::uint32_t(old_loc->at(0)) - 1)};
|
|
if (!check_max_range(new_range))
|
|
return {error::max_subaddresses};
|
|
add_out(major_entry.first, new_range);
|
|
old_loc->at(0) = new_loc->at(0);
|
|
}
|
|
if (old_loc->at(1) < new_loc->at(1))
|
|
{ // overlap at end
|
|
const index_range new_range{minor_index(std::uint32_t(old_loc->at(1)) + 1), new_loc->at(1)};
|
|
if (!check_max_range(new_range))
|
|
return {error::max_subaddresses};
|
|
add_out(major_entry.first, new_range);
|
|
old_loc->at(1) = new_loc->at(1);
|
|
}
|
|
++new_loc;
|
|
}
|
|
}
|
|
|
|
std::copy(old_loc, old_range.end(), std::back_inserter(new_dict.get_container()));
|
|
for ( ; new_loc != new_range.end(); ++new_loc)
|
|
{
|
|
if (!check_max_range(*new_loc))
|
|
return {error::max_subaddresses};
|
|
new_dict.get_container().push_back(*new_loc);
|
|
add_out(major_entry.first, *new_loc);
|
|
}
|
|
}
|
|
|
|
for (const auto& new_indexes : new_dict.get_container())
|
|
{
|
|
for (std::uint64_t minor : boost::counting_range(std::uint64_t(new_indexes[0]), std::uint64_t(new_indexes[1]) + 1))
|
|
{
|
|
subaddress_map new_value{};
|
|
new_value.index = address_index{major_entry.first, minor_index(minor)};
|
|
new_value.subaddress = new_value.index.get_spend_public(address, view_key);
|
|
|
|
value = lmdb::to_val(new_value);
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(indexes_cur.get(), &key, &value, 0));
|
|
}
|
|
}
|
|
|
|
const expect<epee::byte_slice> value_bytes =
|
|
subaddress_ranges.make_value(major_entry.first, new_dict);
|
|
if (!value_bytes)
|
|
return value_bytes.error();
|
|
value = MDB_val{value_bytes->size(), const_cast<void*>(static_cast<const void*>(value_bytes->data()))};
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(ranges_cur.get(), &key, &value, 0));
|
|
}
|
|
|
|
return {std::move(out)};
|
|
});
|
|
}
|
|
|
|
expect<void> storage::add_webhook(const webhook_type type, const boost::optional<account_address>& address, const webhook_value& event)
|
|
{
|
|
if (event.second.url != "zmq")
|
|
{
|
|
epee::net_utils::http::url_content url{};
|
|
if (event.second.url.empty() || !epee::net_utils::parse_url(event.second.url, url))
|
|
return {error::bad_url};
|
|
if (url.schema != "http" && url.schema != "https")
|
|
return {error::bad_url};
|
|
}
|
|
|
|
return db->try_write([this, type, &address, &event] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::webhooks webhooks_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.webhooks, webhooks_cur));
|
|
|
|
webhook_key key{account_id::invalid, type};
|
|
MDB_val lmkey{};
|
|
MDB_val lmvalue{};
|
|
|
|
if (address)
|
|
{
|
|
lmkey = lmdb::to_val(by_address_version);
|
|
lmvalue = lmdb::to_val(*address);
|
|
const int err = mdb_cursor_get(accounts_ba_cur.get(), &lmkey, &lmvalue, MDB_GET_BOTH);
|
|
if (err && err != MDB_NOTFOUND)
|
|
return {lmdb::error(err)};
|
|
if (err != MDB_NOTFOUND)
|
|
key.user = MONERO_UNWRAP(accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup.id)>(lmvalue));
|
|
}
|
|
|
|
if (key.user == account_id::invalid && type == webhook_type::tx_confirmation)
|
|
return {error::bad_webhook};
|
|
|
|
lmkey = lmdb::to_val(key);
|
|
const expect<epee::byte_slice> value = webhooks.make_value(event.first, event.second);
|
|
if (!value)
|
|
return value.error();
|
|
lmvalue = MDB_val{value->size(), const_cast<void*>(static_cast<const void*>(value->data()))};
|
|
MONERO_LMDB_CHECK(mdb_cursor_put(webhooks_cur.get(), &lmkey, &lmvalue, 0));
|
|
return success();
|
|
});
|
|
}
|
|
|
|
expect<void> storage::clear_webhooks(const epee::span<const account_address> addresses)
|
|
{
|
|
if (addresses.empty())
|
|
return success();
|
|
|
|
return db->try_write([this, addresses] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::accounts_by_address accounts_ba_cur;
|
|
cursor::webhooks webhooks_cur;
|
|
cursor::events events_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.accounts_ba, accounts_ba_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.webhooks, webhooks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.events, events_cur));
|
|
|
|
webhook_key key{account_id::invalid, webhook_type::tx_confirmation};
|
|
for (const auto& address : addresses)
|
|
{
|
|
MDB_val lmkey = lmdb::to_val(by_address_version);
|
|
MDB_val lmvalue = lmdb::to_val(address);
|
|
|
|
MONERO_LMDB_CHECK(mdb_cursor_get(accounts_ba_cur.get(), &lmkey, &lmvalue, MDB_GET_BOTH));
|
|
key.user = MONERO_UNWRAP(accounts_by_address.get_value<MONERO_FIELD(account_by_address, lookup.id)>(lmvalue));
|
|
|
|
lmkey = lmdb::to_val(key);
|
|
int err = mdb_cursor_get(webhooks_cur.get(), &lmkey, &lmvalue, MDB_SET);
|
|
if (!err)
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(webhooks_cur.get(), MDB_NODUPDATA));
|
|
|
|
lmkey = lmdb::to_val(key.user);
|
|
err = mdb_cursor_get(events_cur.get(), &lmkey, &lmvalue, MDB_SET);
|
|
if (!err)
|
|
mdb_cursor_del(events_cur.get(), MDB_NODUPDATA);
|
|
}
|
|
|
|
return success();
|
|
});
|
|
}
|
|
|
|
expect<void> storage::clear_webhooks(std::vector<boost::uuids::uuid> ids)
|
|
{
|
|
if (ids.empty())
|
|
return success();
|
|
|
|
std::sort(ids.begin(), ids.end());
|
|
|
|
return db->try_write([this, &ids] (MDB_txn& txn) -> expect<void>
|
|
{
|
|
cursor::webhooks webhooks_cur;
|
|
cursor::events events_cur;
|
|
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.webhooks, webhooks_cur));
|
|
MONERO_CHECK(check_cursor(txn, this->db->tables.events, events_cur));
|
|
|
|
MDB_val key{};
|
|
MDB_val value{};
|
|
int err = mdb_cursor_get(webhooks_cur.get(), &key, &value, MDB_FIRST);
|
|
for ( ; /* every webhook */ ; )
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
const boost::uuids::uuid id =
|
|
MONERO_UNWRAP(webhooks.get_fixed_value<MONERO_FIELD(webhook_dupsort, event_id)>(value));
|
|
if (std::binary_search(ids.begin(), ids.end(), id))
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(webhooks_cur.get(), 0));
|
|
|
|
err = mdb_cursor_get(webhooks_cur.get(), &key, &value, MDB_NEXT);
|
|
}
|
|
|
|
err = mdb_cursor_get(events_cur.get(), &key, &value, MDB_FIRST);
|
|
for ( ; /* every event */ ; )
|
|
{
|
|
if (err)
|
|
{
|
|
if (err == MDB_NOTFOUND)
|
|
break;
|
|
return {lmdb::error(err)};
|
|
}
|
|
|
|
const webhook_dupsort event =
|
|
MONERO_UNWRAP(events_by_account_id.get_value<MONERO_FIELD(webhook_event, link_webhook)>(value));
|
|
if (std::binary_search(ids.begin(), ids.end(), event.event_id))
|
|
MONERO_LMDB_CHECK(mdb_cursor_del(events_cur.get(), 0));
|
|
|
|
err = mdb_cursor_get(events_cur.get(), &key, &value, MDB_NEXT);
|
|
}
|
|
|
|
return success();
|
|
});
|
|
}
|
|
} // db
|
|
} // lws
|