Files
rippled/src/libxrpl/server/Wallet.cpp
Pratik Mankawde bec9e1c8a9 fix(telemetry): resolve the node identity before the Application is built
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.
2026-09-14 20:34:31 +01:00

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