mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
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.
326 lines
10 KiB
C++
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
|