mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
resolveNodePublicKey() returned std::nullopt in three real cases: a first boot with no wallet database, a standalone run (its wallet is a private temporary database), and --newnodeid. Telemetry's resources are built during ApplicationImp's member-init list and are immutable, so on those runs the node reported an empty service.instance.id and no xrpl.node.id for the whole run, while setup() minted a key moments later and patched only the tracer. Replace it with resolveNodeIdentity(), which always returns a keypair: derived from a configured seed, else read from an existing wallet database, else minted. Main.cpp passes that pair to makeApplication(), ApplicationImp stores it in nodeIdentity_ -- now declared before telemetry_ and no longer an optional, because it is always set -- and builds the telemetry resource from it. setup() calls getNodeIdentity(), which now persists rather than mints: it stores the resolved pair when the wallet holds no identity, adopts the stored one when it does, and clears first for --newnodeid. The write stays in setup() because that is where the database exists; a standalone run has no persistent wallet to write to, which is why the pair has to be decided before construction rather than read back afterwards. Wallet gains storeNodeIdentity() for that write, and getNodeIdentity(session) now uses it instead of repeating the insert. The three-argument makeApplication() mints a keypair, so jtx::Env and any other test Application behave as a standalone run always did. Also fold the three hand-rolled "meter from a NoopMeterProvider" copies into telemetry::noopMeter(): the base-pointer call and the kMeterVersion argument are both easy to get wrong alone, and the meter identity has to match the one the histogram views select on. The new gtest covers the wallet half: store-then-read, store not replacing an existing identity, clear-then-store, and that the mint path persists. It adds the tests.libxrpl > xrpl.rdb levelization edge, regenerated here.
330 lines
10 KiB
C++
330 lines
10 KiB
C++
#include <xrpl/server/Wallet.h>
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/UnorderedContainers.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/safe_cast.h>
|
|
#include <xrpl/beast/hash/uhash.h>
|
|
#include <xrpl/beast/utility/Journal.h>
|
|
#include <xrpl/core/PeerReservationTable.h>
|
|
#include <xrpl/protocol/KeyType.h>
|
|
#include <xrpl/protocol/PublicKey.h>
|
|
#include <xrpl/protocol/SecretKey.h>
|
|
#include <xrpl/protocol/tokens.h>
|
|
#include <xrpl/rdb/DBInit.h>
|
|
#include <xrpl/rdb/DatabaseCon.h>
|
|
#include <xrpl/rdb/SociDB.h>
|
|
#include <xrpl/server/Manifest.h>
|
|
|
|
#include <boost/optional/optional.hpp> // IWYU pragma: keep
|
|
|
|
#include <soci/blob-exchange.h> // IWYU pragma: keep
|
|
#include <soci/blob.h>
|
|
#include <soci/boost-optional.h> // IWYU pragma: keep
|
|
#include <soci/into.h>
|
|
#include <soci/session.h>
|
|
#include <soci/statement.h>
|
|
#include <soci/transaction.h>
|
|
#include <soci/use.h>
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <format>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
|
|
namespace xrpl {
|
|
|
|
std::unique_ptr<DatabaseCon>
|
|
makeWalletDB(DatabaseCon::Setup const& setup, beast::Journal j)
|
|
{
|
|
// wallet database
|
|
return std::make_unique<DatabaseCon>(
|
|
setup, kWalletDbName, std::array<std::string, 0>(), kWalletDbInit, j);
|
|
}
|
|
|
|
std::unique_ptr<DatabaseCon>
|
|
makeTestWalletDB(DatabaseCon::Setup const& setup, std::string const& dbname, beast::Journal j)
|
|
{
|
|
// wallet database
|
|
return std::make_unique<DatabaseCon>(
|
|
setup, dbname.data(), std::array<std::string, 0>(), kWalletDbInit, j);
|
|
}
|
|
|
|
void
|
|
getManifests(
|
|
soci::session& session,
|
|
std::string const& dbTable,
|
|
ManifestCache& cache,
|
|
beast::Journal j)
|
|
{
|
|
// Load manifests stored in database
|
|
std::string const sql = "SELECT RawData FROM " + dbTable + ";";
|
|
soci::blob sociRawData(session);
|
|
soci::statement st = (session.prepare << sql, soci::into(sociRawData));
|
|
st.execute();
|
|
while (st.fetch())
|
|
{
|
|
std::string serialized;
|
|
convert(sociRawData, serialized);
|
|
if (auto mo = deserializeManifest(serialized))
|
|
{
|
|
if (!mo->verify())
|
|
{
|
|
JLOG(j.warn()) << "Unverifiable manifest in db";
|
|
continue;
|
|
}
|
|
|
|
// Only trusted manifests are persisted (see saveManifests), so
|
|
// anything loaded from the DB bypasses the untrusted cap.
|
|
cache.applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped);
|
|
}
|
|
else
|
|
{
|
|
JLOG(j.warn()) << "Malformed manifest in database";
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
saveManifest(soci::session& session, std::string const& dbTable, std::string const& serialized)
|
|
{
|
|
// soci does not support bulk insertion of blob data
|
|
// Do not reuse blob because manifest ecdsa signatures vary in length
|
|
// but blob write length is expected to be >= the last write
|
|
soci::blob rawData(session);
|
|
convert(serialized, rawData);
|
|
session << "INSERT INTO " << dbTable << " (RawData) VALUES (:rawData);", soci::use(rawData);
|
|
}
|
|
|
|
void
|
|
saveManifests(
|
|
soci::session& session,
|
|
std::string const& dbTable,
|
|
std::function<bool(PublicKey const&)> const& isTrusted,
|
|
hash_map<PublicKey, Manifest> const& map,
|
|
beast::Journal j)
|
|
{
|
|
soci::transaction tr(session);
|
|
session << "DELETE FROM " << dbTable;
|
|
// Count skipped untrusted manifests and log one summary afterwards, since
|
|
// the cache can hold many and per-entry logging would flood at shutdown.
|
|
std::size_t skipped = 0;
|
|
for (auto const& v : map)
|
|
{
|
|
// Persist only trusted keys. Untrusted gossip is left out so a flood
|
|
// cannot survive a restart on disk.
|
|
if (!isTrusted(v.second.masterKey))
|
|
{
|
|
++skipped;
|
|
continue;
|
|
}
|
|
|
|
saveManifest(session, dbTable, v.second.serialized);
|
|
}
|
|
tr.commit();
|
|
|
|
if (skipped != 0)
|
|
{
|
|
JLOG(j.info()) << skipped << " untrusted manifest(s) in cache not saved to db";
|
|
}
|
|
}
|
|
|
|
void
|
|
addValidatorManifest(soci::session& session, std::string const& serialized)
|
|
{
|
|
soci::transaction tr(session);
|
|
saveManifest(session, "ValidatorManifests", serialized);
|
|
tr.commit();
|
|
}
|
|
|
|
void
|
|
clearNodeIdentity(soci::session& session)
|
|
{
|
|
session << "DELETE FROM NodeIdentity;";
|
|
}
|
|
|
|
std::optional<std::pair<PublicKey, SecretKey>>
|
|
readNodeIdentity(soci::session& session)
|
|
{
|
|
// SOCI requires boost::optional (not std::optional) as the parameter.
|
|
boost::optional<std::string> pubKO, priKO;
|
|
soci::statement st =
|
|
(session.prepare << "SELECT PublicKey, PrivateKey FROM NodeIdentity;",
|
|
soci::into(pubKO),
|
|
soci::into(priKO));
|
|
st.execute();
|
|
while (st.fetch())
|
|
{
|
|
auto const sk = parseBase58<SecretKey>(TokenType::NodePrivate, priKO.value_or(""));
|
|
auto const pk = parseBase58<PublicKey>(TokenType::NodePublic, pubKO.value_or(""));
|
|
|
|
// Only use if the public and secret keys are a pair
|
|
if (sk && pk && (*pk == derivePublicKey(KeyType::Secp256k1, *sk)))
|
|
return std::pair{*pk, *sk};
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void
|
|
storeNodeIdentity(soci::session& session, std::pair<PublicKey, SecretKey> const& keys)
|
|
{
|
|
session << std::format(
|
|
"INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
|
|
"VALUES ('{}','{}');",
|
|
toBase58(TokenType::NodePublic, keys.first),
|
|
toBase58(TokenType::NodePrivate, keys.second));
|
|
}
|
|
|
|
std::pair<PublicKey, SecretKey>
|
|
getNodeIdentity(soci::session& session)
|
|
{
|
|
if (auto const stored = readNodeIdentity(session))
|
|
return *stored;
|
|
|
|
// If a valid identity wasn't found, we randomly generate a new one:
|
|
auto const keys = randomKeyPair(KeyType::Secp256k1);
|
|
storeNodeIdentity(session, keys);
|
|
return keys;
|
|
}
|
|
|
|
std::unordered_set<PeerReservation, beast::Uhash<>, KeyEqual>
|
|
getPeerReservationTable(soci::session& session, beast::Journal j)
|
|
{
|
|
std::unordered_set<PeerReservation, beast::Uhash<>, KeyEqual> table;
|
|
// These values must be boost::optionals (not std) because SOCI expects
|
|
// boost::optionals.
|
|
boost::optional<std::string> valPubKey, valDesc;
|
|
// We should really abstract the table and column names into constants,
|
|
// but no one else does. Because it is too tedious? It would be easy if we
|
|
// had a jOOQ for C++.
|
|
soci::statement st =
|
|
(session.prepare << "SELECT PublicKey, Description FROM PeerReservations;",
|
|
soci::into(valPubKey),
|
|
soci::into(valDesc));
|
|
st.execute();
|
|
while (st.fetch())
|
|
{
|
|
if (!valPubKey || !valDesc)
|
|
{
|
|
// This represents a `NULL` in a `NOT NULL` column. It should be
|
|
// unreachable.
|
|
continue;
|
|
}
|
|
auto const optNodeId = parseBase58<PublicKey>(TokenType::NodePublic, *valPubKey);
|
|
if (!optNodeId)
|
|
{
|
|
JLOG(j.warn()) << "load: not a public key: " << valPubKey;
|
|
continue;
|
|
}
|
|
table.insert(PeerReservation{.nodeId = *optNodeId, .description = *valDesc});
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
void
|
|
insertPeerReservation(
|
|
soci::session& session,
|
|
PublicKey const& nodeId,
|
|
std::string const& description)
|
|
{
|
|
auto const sNodeId = toBase58(TokenType::NodePublic, nodeId);
|
|
session << "INSERT INTO PeerReservations (PublicKey, Description) "
|
|
"VALUES (:nodeId, :desc) "
|
|
"ON CONFLICT (PublicKey) DO UPDATE SET "
|
|
"Description=excluded.Description",
|
|
soci::use(sNodeId), soci::use(description);
|
|
}
|
|
|
|
void
|
|
deletePeerReservation(soci::session& session, PublicKey const& nodeId)
|
|
{
|
|
auto const sNodeId = toBase58(TokenType::NodePublic, nodeId);
|
|
session << "DELETE FROM PeerReservations WHERE PublicKey = :nodeId", soci::use(sNodeId);
|
|
}
|
|
|
|
bool
|
|
createFeatureVotes(soci::session& session)
|
|
{
|
|
soci::transaction tr(session);
|
|
std::string const sql =
|
|
"SELECT count(*) FROM sqlite_master "
|
|
"WHERE type='table' AND name='FeatureVotes'";
|
|
// SOCI requires boost::optional (not std::optional) as the parameter.
|
|
boost::optional<int> featureVotesCount;
|
|
session << sql, soci::into(featureVotesCount);
|
|
bool const exists = static_cast<bool>(*featureVotesCount);
|
|
|
|
// Create FeatureVotes table in WalletDB if it doesn't exist
|
|
if (!exists)
|
|
{
|
|
session << "CREATE TABLE FeatureVotes ( "
|
|
"AmendmentHash CHARACTER(64) NOT NULL, "
|
|
"AmendmentName TEXT, "
|
|
"Veto INTEGER NOT NULL );";
|
|
tr.commit();
|
|
}
|
|
return exists;
|
|
}
|
|
|
|
void
|
|
readAmendments(
|
|
soci::session& session,
|
|
std::function<void(
|
|
boost::optional<std::string> amendmentHash,
|
|
boost::optional<std::string> amendmentName,
|
|
boost::optional<AmendmentVote> vote)> const& callback)
|
|
{
|
|
// lambda that converts the internally stored int to an AmendmentVote.
|
|
auto intToVote = [](boost::optional<int> const& dbVote) -> boost::optional<AmendmentVote> {
|
|
return safeCast<AmendmentVote>(dbVote.value_or(1));
|
|
};
|
|
|
|
soci::transaction const tr(session);
|
|
std::string const sql =
|
|
"SELECT AmendmentHash, AmendmentName, Veto FROM "
|
|
"( SELECT AmendmentHash, AmendmentName, Veto, RANK() OVER "
|
|
"( PARTITION BY AmendmentHash ORDER BY ROWID DESC ) "
|
|
"as rnk FROM FeatureVotes ) WHERE rnk = 1";
|
|
// SOCI requires boost::optional (not std::optional) as parameters.
|
|
boost::optional<std::string> amendmentHash;
|
|
boost::optional<std::string> amendmentName;
|
|
boost::optional<int> voteToVeto;
|
|
soci::statement st =
|
|
(session.prepare << sql,
|
|
soci::into(amendmentHash),
|
|
soci::into(amendmentName),
|
|
soci::into(voteToVeto));
|
|
st.execute();
|
|
while (st.fetch())
|
|
{
|
|
callback(amendmentHash, amendmentName, intToVote(voteToVeto));
|
|
}
|
|
}
|
|
|
|
void
|
|
voteAmendment(
|
|
soci::session& session,
|
|
uint256 const& amendment,
|
|
std::string const& name,
|
|
AmendmentVote vote)
|
|
{
|
|
soci::transaction tr(session);
|
|
std::string sql =
|
|
"INSERT INTO FeatureVotes (AmendmentHash, AmendmentName, Veto) VALUES "
|
|
"('";
|
|
sql += to_string(amendment);
|
|
sql += "', '" + name;
|
|
sql += "', '" + std::to_string(safeCast<int>(vote)) + "');";
|
|
session << sql;
|
|
tr.commit();
|
|
}
|
|
|
|
} // namespace xrpl
|