mirror of
https://github.com/Xahau/xahaud.git
synced 2026-09-15 20:18:29 +00:00
Compare commits
12 Commits
dev
...
fail-fast-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6238cb7109 | ||
|
|
0cd4f6aae4 | ||
|
|
a4b0a90998 | ||
|
|
6981b5b06b | ||
|
|
d4a681430a | ||
|
|
3760e1fb41 | ||
|
|
6442e0990b | ||
|
|
1e6cda4d64 | ||
|
|
c146f15247 | ||
|
|
fb5081d1f4 | ||
|
|
d4bec012a2 | ||
|
|
f7187ba94f |
@@ -19,15 +19,548 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/app/consensus/RCLValidations.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/AmendmentTable.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class AmendmentBlocked_test : public beast::unit_test::suite
|
||||
{
|
||||
// An amendment id this binary will never support.
|
||||
static uint256
|
||||
unsupportedAmendmentId()
|
||||
{
|
||||
std::string const in = "AmendmentBlocked_test.unsupported";
|
||||
sha256_hasher h;
|
||||
using beast::hash_append;
|
||||
hash_append(h, in);
|
||||
auto const d = static_cast<sha256_hasher::result_type>(h);
|
||||
uint256 result;
|
||||
std::memcpy(result.data(), d.data(), d.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
// The majority period only sets how far out activation is expected, so
|
||||
// shorten it from two weeks. That keeps the ledger close-time jumps in
|
||||
// these tests down to minutes, and makes the relationship to the
|
||||
// five-minute shutdown lead time in LedgerMaster obvious.
|
||||
static std::unique_ptr<Config>
|
||||
shortMajorityConfig()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto cfg = test::jtx::envconfig();
|
||||
cfg->AMENDMENT_MAJORITY_TIME = 15min;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Give the amendment table a majority, as of now, for an amendment we do
|
||||
// not support; activation is then expected one majority period out.
|
||||
// Bypasses the ReadView overload (and therefore needValidatedLedger) on
|
||||
// purpose: because lastUpdateSeq_ is set to the current sequence, later
|
||||
// closes within the same 256-ledger block will not recompute -- and so
|
||||
// will not clear -- what we inject here.
|
||||
void
|
||||
injectUnsupportedMajority(test::jtx::Env& env)
|
||||
{
|
||||
auto const seq = env.closed()->info().seq;
|
||||
majorityAmendments_t majority;
|
||||
majority[unsupportedAmendmentId()] = env.now();
|
||||
env.app().getAmendmentTable().doValidatedLedger(seq, {}, majority);
|
||||
|
||||
auto const first =
|
||||
env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
BEAST_EXPECT(
|
||||
first &&
|
||||
*first == env.now() + env.app().config().AMENDMENT_MAJORITY_TIME);
|
||||
}
|
||||
|
||||
void
|
||||
testReceiptFileHelpers()
|
||||
{
|
||||
testcase("amendment blocked receipt helpers");
|
||||
|
||||
auto const id = unsupportedAmendmentId();
|
||||
|
||||
{
|
||||
beast::temp_dir td;
|
||||
Config cfg;
|
||||
cfg.CONFIG_DIR = td.path();
|
||||
|
||||
auto const path = amendmentBlockedFilePath(cfg);
|
||||
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
|
||||
BEAST_EXPECT(
|
||||
path.parent_path() == boost::filesystem::path{td.path()});
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
|
||||
// Nothing to remove yet, and that is not an error.
|
||||
boost::system::error_code ec;
|
||||
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
|
||||
BEAST_EXPECT(!ec);
|
||||
|
||||
BEAST_EXPECT(!writeAmendmentBlockedFile(
|
||||
cfg, {to_string(id) + " (already active)"}));
|
||||
BEAST_EXPECT(boost::filesystem::exists(path));
|
||||
|
||||
auto const contents = getFileContents(ec, path);
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(
|
||||
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") == 0);
|
||||
// When it stopped, what was running, and what it choked on.
|
||||
BEAST_EXPECT(contents.find("Stopped at:") != std::string::npos);
|
||||
BEAST_EXPECT(
|
||||
contents.find(BuildInfo::getVersionString()) !=
|
||||
std::string::npos);
|
||||
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("already active") != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
|
||||
// The receipt is self-clearing, so it must not tell the operator
|
||||
// to delete anything.
|
||||
BEAST_EXPECT(
|
||||
contents.find("removed automatically") != std::string::npos);
|
||||
|
||||
// The timestamp is rendered, not a placeholder: to_string_iso
|
||||
// gives YYYY-MM-DDTHH:MM:SSZ.
|
||||
auto const stampAt = contents.find("Stopped at:");
|
||||
if (BEAST_EXPECT(stampAt != std::string::npos))
|
||||
{
|
||||
auto const eol = contents.find('\n', stampAt);
|
||||
auto const line = contents.substr(stampAt, eol - stampAt);
|
||||
BEAST_EXPECT(line.find("20") != std::string::npos);
|
||||
BEAST_EXPECT(line.find('T') != std::string::npos);
|
||||
BEAST_EXPECT(line.find('Z') != std::string::npos);
|
||||
}
|
||||
|
||||
// Now it can be removed, and removal is reported.
|
||||
ec.clear();
|
||||
BEAST_EXPECT(removeAmendmentBlockedFile(cfg, ec));
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
|
||||
// With no amendments to name the receipt still says something
|
||||
// useful.
|
||||
BEAST_EXPECT(!writeAmendmentBlockedFile(cfg, {}));
|
||||
ec.clear();
|
||||
auto const bare = getFileContents(ec, path);
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(
|
||||
bare.find("not supported by this build") != std::string::npos);
|
||||
}
|
||||
|
||||
// A directory we cannot write to must surface an error rather than
|
||||
// throw. The shutdown continues either way; only the receipt is lost,
|
||||
// which is what happens on installs where CONFIG_DIR is read-only for
|
||||
// the account xahaud runs as.
|
||||
{
|
||||
Config cfg;
|
||||
cfg.CONFIG_DIR =
|
||||
boost::filesystem::path{"/"} / "no" / "such" / "directory";
|
||||
BEAST_EXPECT(!!writeAmendmentBlockedFile(cfg, {}));
|
||||
|
||||
boost::system::error_code ec;
|
||||
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testStandaloneDoesNotStop()
|
||||
{
|
||||
testcase("standalone does not stop or leave a receipt");
|
||||
using namespace test::jtx;
|
||||
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
cfg->CONFIG_DIR = td.path();
|
||||
return cfg;
|
||||
})};
|
||||
BEAST_EXPECT(env.app().config().standalone());
|
||||
|
||||
auto const path = amendmentBlockedFilePath(env.app().config());
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
}
|
||||
|
||||
void
|
||||
testShutdownOnBlock()
|
||||
{
|
||||
testcase("amendment blocked stops the server");
|
||||
using namespace test::jtx;
|
||||
|
||||
// A non-standalone Env is needed to exercise shutdown rather than
|
||||
// only setting the blocked flag. Consequences when editing:
|
||||
// setup() arms the state timer, run() arms the deadlock detector, and
|
||||
// signalStop() below releases run() to tear the application down
|
||||
// concurrently with the rest of this function. Do all the assertions
|
||||
// straight away and let the Env go out of scope promptly.
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> config) {
|
||||
config->NODE_SIZE = 0;
|
||||
config->setupControl(true, true, false);
|
||||
// setupControl picks a production node size for
|
||||
// non-standalone; put it back to "tiny" for the test.
|
||||
config->NODE_SIZE = 0;
|
||||
config->CONFIG_DIR = td.path();
|
||||
config->legacy("database_path", td.path());
|
||||
return config;
|
||||
})};
|
||||
BEAST_EXPECT(!env.app().config().standalone());
|
||||
|
||||
auto const path = amendmentBlockedFilePath(env.app().config());
|
||||
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
|
||||
// Make the table report an unsupported amendment so the receipt has
|
||||
// something concrete to name.
|
||||
auto const id = unsupportedAmendmentId();
|
||||
env.app().getAmendmentTable().enable(id);
|
||||
BEAST_EXPECT(env.app().getAmendmentTable().hasUnsupportedEnabled());
|
||||
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(env.app().isStopping());
|
||||
BEAST_EXPECT(boost::filesystem::exists(path));
|
||||
|
||||
boost::system::error_code readError;
|
||||
auto const contents = getFileContents(readError, path);
|
||||
BEAST_EXPECT(!readError);
|
||||
BEAST_EXPECT(
|
||||
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") !=
|
||||
std::string::npos);
|
||||
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
|
||||
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("already active") != std::string::npos);
|
||||
|
||||
// Repeat calls are a no-op: Change::applyAmendment reaches this from
|
||||
// the transaction apply path without an isBlocked() guard, so it must
|
||||
// not rewrite the receipt or re-log once per ledger.
|
||||
BEAST_EXPECT(boost::filesystem::remove(path));
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
}
|
||||
|
||||
void
|
||||
checkJump(bool unsupported)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
cfg->setupControl(true, true, false);
|
||||
cfg->NODE_SIZE = 0;
|
||||
cfg->CONFIG_DIR = td.path();
|
||||
cfg->legacy("database_path", td.path());
|
||||
return cfg;
|
||||
})};
|
||||
auto& app = env.app();
|
||||
auto& ops = app.getOPs();
|
||||
auto& master = app.getLedgerMaster();
|
||||
std::lock_guard lock(app.getMasterMutex());
|
||||
auto const before = master.getClosedLedger();
|
||||
|
||||
// A direct child is not preferred over our LCL: consensus may be
|
||||
// about to build it. Two ledgers ahead forces the JUMP path.
|
||||
auto parent =
|
||||
std::make_shared<Ledger>(*before, app.timeKeeper().closeTime());
|
||||
parent->updateSkipList();
|
||||
parent->setImmutable();
|
||||
auto candidate =
|
||||
std::make_shared<Ledger>(*parent, app.timeKeeper().closeTime());
|
||||
candidate->updateSkipList();
|
||||
|
||||
if (unsupported)
|
||||
{
|
||||
auto const key = keylet::amendments();
|
||||
auto const existing = candidate->read(key);
|
||||
auto sle = existing ? std::make_shared<SLE>(*existing)
|
||||
: std::make_shared<SLE>(key);
|
||||
STVector256 amendments;
|
||||
if (sle->isFieldPresent(sfAmendments))
|
||||
amendments = sle->getFieldV256(sfAmendments);
|
||||
amendments.push_back(unsupportedAmendmentId());
|
||||
sle->setFieldV256(sfAmendments, amendments);
|
||||
if (existing)
|
||||
candidate->rawReplace(sle);
|
||||
else
|
||||
candidate->rawInsert(sle);
|
||||
}
|
||||
|
||||
// A serialized uint32 with an unknown field number. Insert raw bytes
|
||||
// so the real transaction parser, reached through TxQ, must throw.
|
||||
BEAST_EXPECT(SField::getField(STI_UINT32, 255).isInvalid());
|
||||
auto tx = std::make_shared<Serializer>();
|
||||
tx->add8(0x20);
|
||||
tx->add8(255);
|
||||
while (tx->getDataLength() < txMinSizeBytes)
|
||||
tx->add8(0);
|
||||
auto meta = std::make_shared<Serializer>();
|
||||
meta->add8(0xE1);
|
||||
auto const txID = sha512Half(tx->slice());
|
||||
candidate->rawTxInsert(txID, tx, meta);
|
||||
candidate->setImmutable();
|
||||
|
||||
bool unknownField = false;
|
||||
try
|
||||
{
|
||||
candidate->txRead(txID);
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
unknownField = std::string(e.what()).starts_with("Unknown field");
|
||||
}
|
||||
if (!BEAST_EXPECT(unknownField))
|
||||
return;
|
||||
|
||||
master.storeLedger(candidate);
|
||||
auto const keys = randomKeyPair(KeyType::secp256k1);
|
||||
auto const nodeID = calcNodeID(keys.first);
|
||||
auto validation = std::make_shared<STValidation>(
|
||||
app.timeKeeper().closeTime(),
|
||||
keys.first,
|
||||
keys.second,
|
||||
nodeID,
|
||||
[&](STValidation& v) {
|
||||
v.setFieldH256(sfLedgerHash, candidate->info().hash);
|
||||
v.setFieldU32(sfLedgerSequence, candidate->seq());
|
||||
});
|
||||
// Add directly so ledger acceptance does not discover the amendment
|
||||
// before JUMP has a chance to inspect the candidate itself.
|
||||
BEAST_EXPECT(
|
||||
app.getValidations().add(nodeID, RCLValidation{validation}) ==
|
||||
ValStatus::current);
|
||||
if (!BEAST_EXPECT(
|
||||
app.getValidations().getPreferredLCL(
|
||||
RCLValidatedLedger{before, env.journal},
|
||||
master.getValidLedgerIndex(),
|
||||
{}) == candidate->info().hash))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(!app.getAmendmentTable().hasUnsupportedEnabled());
|
||||
BEAST_EXPECT(!app.getAmendmentTable().firstUnsupportedExpected());
|
||||
BEAST_EXPECT(!ops.isAmendmentBlocked());
|
||||
BEAST_EXPECT(!app.isStopping());
|
||||
auto const receipt = amendmentBlockedFilePath(app.config());
|
||||
BEAST_EXPECT(!boost::filesystem::exists(receipt));
|
||||
|
||||
bool threw = false;
|
||||
try
|
||||
{
|
||||
ops.endConsensus({});
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
threw = true;
|
||||
BEAST_EXPECT(std::string(e.what()).starts_with("Unknown field"));
|
||||
}
|
||||
BEAST_EXPECT(threw == !unsupported);
|
||||
BEAST_EXPECT(ops.isAmendmentBlocked() == unsupported);
|
||||
BEAST_EXPECT(app.isStopping() == unsupported);
|
||||
BEAST_EXPECT(boost::filesystem::exists(receipt) == unsupported);
|
||||
BEAST_EXPECT(
|
||||
master.getClosedLedger()->info().hash == before->info().hash);
|
||||
}
|
||||
|
||||
void
|
||||
testJumpStopsForUnsupportedAmendment()
|
||||
{
|
||||
testcase("JUMP to an unsupported ledger stops the server");
|
||||
checkJump(/*unsupported=*/true);
|
||||
}
|
||||
|
||||
void
|
||||
testJumpRethrowsParsingError()
|
||||
{
|
||||
testcase("JUMP parsing errors without unsupported amendments escape");
|
||||
checkJump(/*unsupported=*/false);
|
||||
}
|
||||
|
||||
void
|
||||
testWarnsOutsideShutdownWindow()
|
||||
{
|
||||
testcase("unsupported majority warns while activation is distant");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
BEAST_EXPECT(env.close());
|
||||
|
||||
// A majority period out: warn, keep running.
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
|
||||
|
||||
// The table can name the amendment, and reports it as pending rather
|
||||
// than active. This is what ends up in the receipt.
|
||||
auto const unsupported =
|
||||
env.app().getAmendmentTable().unsupportedAmendments();
|
||||
BEAST_EXPECT(unsupported.size() == 1);
|
||||
if (unsupported.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(unsupported[0].id == unsupportedAmendmentId());
|
||||
BEAST_EXPECT(
|
||||
unsupported[0].expected ==
|
||||
env.app().getAmendmentTable().firstUnsupportedExpected());
|
||||
}
|
||||
|
||||
// Because firstUnsupportedExpected() is set, the warning carries the
|
||||
// expected activation date.
|
||||
auto const si = env.rpc("server_info")[jss::result];
|
||||
BEAST_EXPECT(si.isMember(jss::info));
|
||||
auto const& warnings = si[jss::info][jss::warnings];
|
||||
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
|
||||
if (warnings.isArray() && warnings.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
|
||||
auto const& details = warnings[0u][jss::details];
|
||||
BEAST_EXPECT(details.isMember(jss::expected_date));
|
||||
BEAST_EXPECT(details.isMember(jss::expected_date_UTC));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUnsupportedAmendmentReporting()
|
||||
{
|
||||
testcase("unsupported amendments are reported for the receipt");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
auto& table = env.app().getAmendmentTable();
|
||||
BEAST_EXPECT(table.unsupportedAmendments().empty());
|
||||
|
||||
// Majority, not yet active -> reported with an expected time.
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
auto pending = table.unsupportedAmendments();
|
||||
BEAST_EXPECT(pending.size() == 1);
|
||||
if (pending.size() == 1)
|
||||
BEAST_EXPECT(pending[0].expected.has_value());
|
||||
|
||||
// Majority lost -> nothing to report. The list is recomputed from the
|
||||
// ledger each time, so it must not accumulate.
|
||||
auto const seq = env.closed()->info().seq;
|
||||
table.doValidatedLedger(seq, {}, {});
|
||||
BEAST_EXPECT(table.unsupportedAmendments().empty());
|
||||
BEAST_EXPECT(!table.firstUnsupportedExpected());
|
||||
|
||||
// Active -> reported with no expected time.
|
||||
BEAST_EXPECT(table.enable(unsupportedAmendmentId()));
|
||||
BEAST_EXPECT(table.hasUnsupportedEnabled());
|
||||
auto const active = table.unsupportedAmendments();
|
||||
BEAST_EXPECT(active.size() == 1);
|
||||
if (active.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(active[0].id == unsupportedAmendmentId());
|
||||
BEAST_EXPECT(!active[0].expected);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWarningVisibleWithoutAdmin()
|
||||
{
|
||||
testcase("unsupported majority warning is not admin only");
|
||||
using namespace test::jtx;
|
||||
|
||||
// No closes here: ledger_accept is Role::ADMIN, server_info is
|
||||
// Role::USER, which is the whole point of the test.
|
||||
Env env{*this, envconfig(no_admin)};
|
||||
env.app().getOPs().setAmendmentWarned();
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
|
||||
auto const si = env.rpc("server_info")[jss::result];
|
||||
BEAST_EXPECT(si.isMember(jss::info));
|
||||
auto const& warnings = si[jss::info][jss::warnings];
|
||||
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
|
||||
if (warnings.isArray() && warnings.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testShutsDownInsideShutdownWindow()
|
||||
{
|
||||
testcase("unsupported majority stops the server before activation");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
|
||||
// First close only warns: activation is still a majority period away.
|
||||
BEAST_EXPECT(env.close());
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
|
||||
|
||||
auto const expected =
|
||||
*env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
// Close two minutes short of the expected activation time. This is not
|
||||
// a flag ledger and the warning has already been issued, so the check
|
||||
// has to run on every validated ledger to see this at all.
|
||||
BEAST_EXPECT(env.close(expected - NetClock::duration{120}));
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
|
||||
// Standalone, so the flag is set but the server keeps running.
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
}
|
||||
|
||||
void
|
||||
testShutsDownWhenActivationOverdue()
|
||||
{
|
||||
testcase("unsupported majority stops the server when overdue");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
|
||||
auto const expected =
|
||||
*env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
// firstUnsupportedExpected() is a lower bound: the amendment actually
|
||||
// activates at the first flag ledger at or after it, so the server can
|
||||
// legitimately still be running an hour past it. That is the most
|
||||
// dangerous state, not the safest, and must stop the server.
|
||||
BEAST_EXPECT(env.close(expected + NetClock::duration{3600}));
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
}
|
||||
|
||||
void
|
||||
testBlockedMethods()
|
||||
{
|
||||
@@ -250,6 +783,16 @@ public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testReceiptFileHelpers();
|
||||
testStandaloneDoesNotStop();
|
||||
testShutdownOnBlock();
|
||||
testJumpRethrowsParsingError();
|
||||
testJumpStopsForUnsupportedAmendment();
|
||||
testWarnsOutsideShutdownWindow();
|
||||
testUnsupportedAmendmentReporting();
|
||||
testWarningVisibleWithoutAdmin();
|
||||
testShutsDownInsideShutdownWindow();
|
||||
testShutsDownWhenActivationOverdue();
|
||||
testBlockedMethods();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,14 +289,49 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
app_.getSHAMapStore().onLedgerClosed(getValidatedLedger());
|
||||
mLedgerHistory.validatedLedger(l, consensusHash);
|
||||
app_.getAmendmentTable().doValidatedLedger(l);
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// How far ahead of the expected activation time to stop the server.
|
||||
// firstUnsupportedExpected() is only a lower bound on activation: the
|
||||
// amendment goes live at the first flag ledger at or after that time, so
|
||||
// stopping early is harmless while stopping late risks being handed a
|
||||
// ledger we cannot deserialize.
|
||||
constexpr auto amendmentShutdownLeadTime = 5min;
|
||||
|
||||
if (!app_.getOPs().isBlocked())
|
||||
{
|
||||
auto const firstUnsupported =
|
||||
app_.getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
if (app_.getAmendmentTable().hasUnsupportedEnabled())
|
||||
{
|
||||
JLOG(m_journal.error()) << "One or more unsupported amendments "
|
||||
"activated: server blocked.";
|
||||
app_.getOPs().setAmendmentBlocked();
|
||||
}
|
||||
else if (
|
||||
firstUnsupported &&
|
||||
app_.timeKeeper().closeTime() + amendmentShutdownLeadTime >=
|
||||
*firstUnsupported)
|
||||
{
|
||||
// Activation is imminent, or the expected time has already passed
|
||||
// and we are only waiting on the next flag ledger. Shut down now,
|
||||
// while we can still deserialize the ledgers we are handed.
|
||||
//
|
||||
// This is deliberately checked on every validated ledger rather
|
||||
// than only on flag ledgers: the lead time above is much shorter
|
||||
// than the flag ledger interval, so a flag-ledger-only check would
|
||||
// usually skip straight over the window.
|
||||
//
|
||||
// The comparison must not be written as (*first - now), because
|
||||
// NetClock::rep is unsigned and wraps once the expected time is in
|
||||
// the past -- which is the most dangerous case, not the safest.
|
||||
JLOG(m_journal.error())
|
||||
<< "Unsupported amendment expected to activate at "
|
||||
<< to_string(*firstUnsupported) << ". Shutting down.";
|
||||
app_.getOPs().setAmendmentBlocked();
|
||||
}
|
||||
else if (!app_.getOPs().isAmendmentWarned() || l->isFlagLedger())
|
||||
{
|
||||
// Amendments can lose majority, so re-check periodically (every
|
||||
@@ -308,12 +343,11 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
// this message may be logged more than once per session, because
|
||||
// the node will otherwise function normally, and this gives
|
||||
// operators an opportunity to see and resolve the warning.
|
||||
if (auto const first =
|
||||
app_.getAmendmentTable().firstUnsupportedExpected())
|
||||
if (firstUnsupported)
|
||||
{
|
||||
JLOG(m_journal.error()) << "One or more unsupported amendments "
|
||||
"reached majority. Upgrade before "
|
||||
<< to_string(*first)
|
||||
<< to_string(*firstUnsupported)
|
||||
<< " to prevent your server from "
|
||||
"becoming amendment blocked.";
|
||||
app_.getOPs().setAmendmentWarned();
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/ResolverAsio.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
@@ -2372,6 +2373,65 @@ Application::Application() : beast::PropertyStream::Source("app")
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
boost::filesystem::path
|
||||
amendmentBlockedFilePath(Config const& config)
|
||||
{
|
||||
return config.CONFIG_DIR / "README_AMENDMENT_BLOCKED";
|
||||
}
|
||||
|
||||
boost::system::error_code
|
||||
writeAmendmentBlockedFile(
|
||||
Config const& config,
|
||||
std::vector<std::string> const& amendments)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << "XAHAUD STOPPED: UPGRADE REQUIRED\n"
|
||||
<< "\n"
|
||||
<< "Stopped at: "
|
||||
<< to_string_iso(time_point_cast<seconds>(system_clock::now())) << "\n"
|
||||
<< "This build: " << BuildInfo::getVersionString() << "\n"
|
||||
<< "\n";
|
||||
|
||||
if (amendments.empty())
|
||||
{
|
||||
ss << "One or more network amendments are not supported by this "
|
||||
"build.\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << "Amendments this build does not support:\n";
|
||||
for (auto const& amendment : amendments)
|
||||
ss << " " << amendment << "\n";
|
||||
}
|
||||
|
||||
ss << "\n"
|
||||
<< "The network has moved to rules this build does not implement, so "
|
||||
"the\n"
|
||||
<< "server stopped rather than keep serving ledgers it cannot read.\n"
|
||||
<< "\n"
|
||||
<< "To get back in sync:\n"
|
||||
<< "1. Upgrade xahaud to a version that supports the amendments "
|
||||
"above.\n"
|
||||
<< "2. Start xahaud again. This file is removed automatically on "
|
||||
"start.\n"
|
||||
<< "\n"
|
||||
<< "Starting this build again without upgrading will stop the server "
|
||||
"again\n"
|
||||
<< "and rewrite this file. Nothing needs to be deleted by hand.\n";
|
||||
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(ec, amendmentBlockedFilePath(config), ss.str());
|
||||
return ec;
|
||||
}
|
||||
|
||||
bool
|
||||
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec)
|
||||
{
|
||||
return boost::filesystem::remove(amendmentBlockedFilePath(config), ec);
|
||||
}
|
||||
|
||||
std::unique_ptr<Application>
|
||||
make_Application(
|
||||
std::unique_ptr<Config> config,
|
||||
|
||||
@@ -27,9 +27,14 @@
|
||||
#include <xrpl/beast/utility/PropertyStream.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -279,6 +284,27 @@ make_Application(
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper);
|
||||
|
||||
/** Location of the receipt left behind when the server stops because it does
|
||||
not support a network amendment. */
|
||||
boost::filesystem::path
|
||||
amendmentBlockedFilePath(Config const& config);
|
||||
|
||||
/** Write the amendment-blocked receipt: a record for the operator of when the
|
||||
server stopped and which amendments it could not support. `amendments`
|
||||
holds one already-rendered line per unsupported amendment, and may be
|
||||
empty. Best effort -- any error is returned rather than thrown, and the
|
||||
caller is expected to continue shutting down either way. */
|
||||
boost::system::error_code
|
||||
writeAmendmentBlockedFile(
|
||||
Config const& config,
|
||||
std::vector<std::string> const& amendments);
|
||||
|
||||
/** Remove any amendment-blocked receipt left behind by a previous run.
|
||||
Returns true if a receipt was present and has been removed; sets `ec` if
|
||||
removal was attempted and failed. */
|
||||
bool
|
||||
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -809,6 +809,31 @@ run(int argc, char** argv)
|
||||
// No arguments. Run server.
|
||||
if (!vm.count("parameters"))
|
||||
{
|
||||
// Clear any receipt left by a previous amendment-blocked shutdown. It
|
||||
// records why the server stopped; it is not a lock. If this build
|
||||
// still does not support the amendment it will stop again and write a
|
||||
// fresh one, so there is nothing for the operator to delete by hand
|
||||
// and no way for a stale receipt to keep a working build down.
|
||||
// Standalone does not write receipts, so it does not clear them
|
||||
// either, leaving the file readable for diagnosis.
|
||||
if (!config->standalone())
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
auto const blockedFile = amendmentBlockedFilePath(*config);
|
||||
if (removeAmendmentBlockedFile(*config, ec))
|
||||
{
|
||||
JLOG(logs->journal("Application").warn())
|
||||
<< "Removed amendment-blocked receipt " << blockedFile
|
||||
<< " left by a previous run.";
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
JLOG(logs->journal("Application").warn())
|
||||
<< "Could not remove amendment-blocked receipt "
|
||||
<< blockedFile << ": " << ec.message();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this comment can be removed in a future release -
|
||||
// say 1.7 or higher
|
||||
if (config->had_trailing_comments())
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -50,6 +51,17 @@ public:
|
||||
VoteBehavior const vote;
|
||||
};
|
||||
|
||||
/** An amendment seen on the network that this server has no code
|
||||
support for. */
|
||||
struct UnsupportedAmendment
|
||||
{
|
||||
uint256 id;
|
||||
|
||||
/** The time the amendment is expected to activate. Unset if it is
|
||||
already active. */
|
||||
std::optional<NetClock::time_point> expected;
|
||||
};
|
||||
|
||||
virtual ~AmendmentTable() = default;
|
||||
|
||||
virtual uint256
|
||||
@@ -80,6 +92,12 @@ public:
|
||||
virtual std::optional<NetClock::time_point>
|
||||
firstUnsupportedExpected() const = 0;
|
||||
|
||||
/** Amendments this server does not support that are already enabled, or
|
||||
that have reached majority and are expected to activate. Ordered by
|
||||
amendment id. Intended for operator-facing diagnostics. */
|
||||
virtual std::vector<UnsupportedAmendment>
|
||||
unsupportedAmendments() const = 0;
|
||||
|
||||
virtual Json::Value
|
||||
getJson(bool isAdmin) const = 0;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/main/LoadManager.h>
|
||||
#include <xrpld/app/misc/AmendmentTable.h>
|
||||
#include <xrpld/app/misc/DeliverMax.h>
|
||||
@@ -1701,11 +1702,68 @@ NetworkOPsImp::isAmendmentBlocked()
|
||||
return amendmentBlocked_;
|
||||
}
|
||||
|
||||
// Render the unsupported amendments for the operator-facing receipt. This is
|
||||
// the only identification available: an amendment this build does not support
|
||||
// has no name here, so the id is what the operator matches against the
|
||||
// release notes.
|
||||
static std::vector<std::string>
|
||||
describeUnsupportedAmendments(AmendmentTable const& table)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
|
||||
for (auto const& amendment : table.unsupportedAmendments())
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << to_string(amendment.id);
|
||||
if (amendment.expected)
|
||||
ss << " (expected to activate "
|
||||
<< to_string_iso(*amendment.expected) << ")";
|
||||
else
|
||||
ss << " (already active)";
|
||||
lines.push_back(ss.str());
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::setAmendmentBlocked()
|
||||
{
|
||||
amendmentBlocked_ = true;
|
||||
// Idempotent: this is reached from Change::applyAmendment (i.e. from the
|
||||
// transaction application path, which is not guarded by isBlocked()) as
|
||||
// well as from LedgerMaster::setValidLedger. Writing the receipt and
|
||||
// logging once per process is enough, and it keeps the synchronous file
|
||||
// write out of any subsequent ledger apply.
|
||||
if (amendmentBlocked_.exchange(true))
|
||||
return;
|
||||
|
||||
setMode(OperatingMode::CONNECTED);
|
||||
if (!app_.config().standalone())
|
||||
{
|
||||
auto const blockedFile = amendmentBlockedFilePath(app_.config());
|
||||
if (auto const ec = writeAmendmentBlockedFile(
|
||||
app_.config(),
|
||||
describeUnsupportedAmendments(app_.getAmendmentTable())))
|
||||
{
|
||||
JLOG(m_journal.fatal())
|
||||
<< "Could not write amendment-blocked receipt " << blockedFile
|
||||
<< ": " << ec.message();
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(m_journal.fatal())
|
||||
<< "Amendment-blocked receipt written to " << blockedFile;
|
||||
}
|
||||
JLOG(m_journal.fatal())
|
||||
<< "This version of xahaud does not support a network amendment. "
|
||||
"The amendment will activate soon or is already active. "
|
||||
"The server will stop. Upgrade xahaud before you restart the "
|
||||
"server.";
|
||||
app_.signalStop(
|
||||
"Unsupported network amendment. Upgrade xahaud before you restart "
|
||||
"the server. Details: " +
|
||||
blockedFile.string());
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
@@ -1851,6 +1909,27 @@ NetworkOPsImp::checkLastClosedLedger(
|
||||
return true;
|
||||
}
|
||||
|
||||
// True if `view` enables an amendment this binary does not implement. Reads
|
||||
// only the amendments ledger entry, so it is safe to call once transaction
|
||||
// deserialization is already known to be failing.
|
||||
static bool
|
||||
ledgerHasUnsupportedAmendments(
|
||||
AmendmentTable const& table,
|
||||
ReadView const& view)
|
||||
{
|
||||
auto const sle = view.read(keylet::amendments());
|
||||
if (!sle || !sle->isFieldPresent(sfAmendments))
|
||||
return false;
|
||||
|
||||
for (auto const& amendment : sle->getFieldV256(sfAmendments))
|
||||
{
|
||||
if (!table.isSupported(amendment))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::switchLastClosedLedger(
|
||||
std::shared_ptr<Ledger const> const& newLCL)
|
||||
@@ -1861,8 +1940,32 @@ NetworkOPsImp::switchLastClosedLedger(
|
||||
|
||||
clearNeedNetworkLedger();
|
||||
|
||||
// Update fee computations.
|
||||
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
|
||||
// Update fee computations. May throw if the ledger contains
|
||||
// transactions with fields unknown to this binary (e.g. after an
|
||||
// unsupported amendment activates). Catch to allow graceful shutdown.
|
||||
try
|
||||
{
|
||||
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// Do not decide this on amendmentBlocked_ alone. A JUMP can happen
|
||||
// before any validated ledger has been processed -- e.g. immediately
|
||||
// after a restart, which is precisely the case that crashed -- so the
|
||||
// flag may still be clear here. Ask the ledger itself instead.
|
||||
// Anything else is a real bug and must propagate.
|
||||
if (!amendmentBlocked_ &&
|
||||
!ledgerHasUnsupportedAmendments(app_.getAmendmentTable(), *newLCL))
|
||||
throw;
|
||||
|
||||
JLOG(m_journal.error()) << "Failed to process closed ledger "
|
||||
<< newLCL->info().seq << ": " << e.what();
|
||||
|
||||
// No-op if we are already blocked; otherwise this starts the
|
||||
// shutdown that should have been started before activation.
|
||||
setAmendmentBlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
// Caller must own master lock
|
||||
{
|
||||
@@ -2542,7 +2645,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
|
||||
"may be incorrectly configured or some [validator_list_sites] "
|
||||
"may be unreachable.";
|
||||
}
|
||||
if (admin && isAmendmentWarned())
|
||||
if (isAmendmentWarned())
|
||||
{
|
||||
Json::Value& w = warnings.append(Json::objectValue);
|
||||
w[jss::id] = warnRPC_UNSUPPORTED_MAJORITY;
|
||||
|
||||
@@ -432,6 +432,11 @@ private:
|
||||
// will be enabled.
|
||||
std::optional<NetClock::time_point> firstUnsupportedExpected_;
|
||||
|
||||
// Unsupported amendments that have reached majority, and the time each
|
||||
// is expected to activate. Recomputed alongside
|
||||
// firstUnsupportedExpected_, so it clears when majority is lost.
|
||||
std::vector<std::pair<uint256, NetClock::time_point>> unsupportedMajority_;
|
||||
|
||||
beast::Journal const j_;
|
||||
|
||||
// Database which persists veto/unveto vote
|
||||
@@ -495,6 +500,9 @@ public:
|
||||
std::optional<NetClock::time_point>
|
||||
firstUnsupportedExpected() const override;
|
||||
|
||||
std::vector<UnsupportedAmendment>
|
||||
unsupportedAmendments() const override;
|
||||
|
||||
Json::Value
|
||||
getJson(bool isAdmin) const override;
|
||||
Json::Value
|
||||
@@ -807,6 +815,36 @@ AmendmentTableImpl::firstUnsupportedExpected() const
|
||||
return firstUnsupportedExpected_;
|
||||
}
|
||||
|
||||
std::vector<AmendmentTable::UnsupportedAmendment>
|
||||
AmendmentTableImpl::unsupportedAmendments() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
std::vector<UnsupportedAmendment> result;
|
||||
|
||||
// Already active.
|
||||
for (auto const& [id, state] : amendmentMap_)
|
||||
{
|
||||
if (state.enabled && !state.supported)
|
||||
result.push_back({id, std::nullopt});
|
||||
}
|
||||
|
||||
// Reached majority, not yet active.
|
||||
for (auto const& entry : unsupportedMajority_)
|
||||
{
|
||||
if (std::none_of(result.begin(), result.end(), [&entry](auto const& u) {
|
||||
return u.id == entry.first;
|
||||
}))
|
||||
result.push_back({entry.first, entry.second});
|
||||
}
|
||||
|
||||
std::sort(result.begin(), result.end(), [](auto const& a, auto const& b) {
|
||||
return a.id < b.id;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
|
||||
{
|
||||
@@ -967,6 +1005,7 @@ AmendmentTableImpl::doValidatedLedger(
|
||||
// if it's currently set. If it's not set when the loop is done, then any
|
||||
// prior unknown amendments have lost majority.
|
||||
firstUnsupportedExpected_.reset();
|
||||
unsupportedMajority_.clear();
|
||||
for (auto const& [hash, time] : majority)
|
||||
{
|
||||
AmendmentState& s = add(hash, lock);
|
||||
@@ -978,6 +1017,7 @@ AmendmentTableImpl::doValidatedLedger(
|
||||
{
|
||||
JLOG(j_.info()) << "Unsupported amendment " << hash
|
||||
<< " reached majority at " << to_string(time);
|
||||
unsupportedMajority_.emplace_back(hash, time + majorityTime_);
|
||||
if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
|
||||
firstUnsupportedExpected_ = time;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user