Files
rippled/src/libxrpl/server/Wallet.cpp
Pratik Mankawde 4278014ab0 fix(telemetry): order the metrics pipeline by instrument kind
beast::insight instruments are created during ApplicationImp's member-init
list, and opentelemetry-cpp 1.28 never rebinds an already-vended Meter, so an
instrument created before the MeterProvider is published records nothing for
the rest of the process. Observable instruments carry the opposite constraint:
registering one arms the SDK reader thread, and its callbacks run hook handlers
that read services which do not exist that early.

Publish the provider in Telemetry's constructor, ahead of every producer, and
defer only the observables. Collector gains onCollectionReady() and
onCollectionStopping(); OTelCollector arms and disarms its gauges in response.
StatsDCollector starts its polling thread in its own constructor and had the
same hazard, so it uses the pair to gate that thread.

The metrics resource carries service.instance.id and is immutable once built,
so the node public key is resolved in Main.cpp, where a config error can still
be reported, and passed to makeApplication(). getNodeIdentity() remains
authoritative; both paths now share readNodeIdentity(), so telemetry cannot
report a key the node has abandoned.

An explicit ~ApplicationImp stops observing and stops telemetry, covering the
setup() failure paths that never reach run(). Telemetry::stop() is once-only
and no longer clears another instance's global pointer. The histogram view's
meter selector now matches the meter actually in use, so its bucket boundaries
apply for the first time.
2026-08-20 16:36:41 +01:00

326 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;
}
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 [newpublicKey, newsecretKey] = randomKeyPair(KeyType::Secp256k1);
session << std::format(
"INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
"VALUES ('{}','{}');",
toBase58(TokenType::NodePublic, newpublicKey),
toBase58(TokenType::NodePrivate, newsecretKey));
return {newpublicKey, newsecretKey};
}
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