test: improve export rng coverage

This commit is contained in:
Nicholas Dudfield
2026-06-09 19:56:46 +07:00
parent 16a72172b4
commit dd21024c0e
14 changed files with 2116 additions and 92 deletions

View File

@@ -16,6 +16,7 @@
*/
//==============================================================================
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
@@ -116,16 +117,11 @@ makeBlob(uint256 const& txHash, PublicKey const& pk, Buffer const& sig)
return makeBlob(txHash, pk, Slice(sig.data(), sig.size()));
}
beast::Journal
journal()
{
return beast::Journal{beast::Journal::getNullSink()};
}
} // namespace
class ExportSignatureHarvester_test : public beast::unit_test::suite
{
SuiteJournal journal_{"ExportSignatureHarvester_test", *this};
std::pair<PublicKey, SecretKey> const sender_ =
randomKeyPair(KeyType::secp256k1);
std::pair<PublicKey, SecretKey> const other_ =
@@ -139,7 +135,8 @@ class ExportSignatureHarvester_test : public beast::unit_test::suite
ExportTxnLookup const& exportTxns,
bool active = true,
std::optional<uint256> sourceLedgerHash = std::nullopt,
PublicKey const* sender = nullptr) const
PublicKey const* sender = nullptr,
std::size_t maxEntries = 2) const
{
return ExportSignatureHarvestInput{
sender ? *sender : sender_.first,
@@ -150,7 +147,13 @@ class ExportSignatureHarvester_test : public beast::unit_test::suite
exportTxns,
42,
source_,
2};
maxEntries};
}
beast::Journal&
journal()
{
return journal_;
}
public:
@@ -238,7 +241,8 @@ public:
ExportTxnLookup lookup;
ExportSigCollector collector;
auto input = makeInput(blobs, lookup, true, prevLedger_);
auto input =
makeInput(blobs, lookup, true, prevLedger_, nullptr, blobs.size());
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
BEAST_EXPECT(collector.signatureCount(txHash) == 0);

View File

@@ -23,6 +23,7 @@
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/misc/RuntimeConfig.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ExportLimits.h>
@@ -51,6 +52,12 @@ struct Export_test : public beast::unit_test::suite
return cfg;
}
static void
forceNonStandalone(Application& app)
{
const_cast<Config&>(app.config()).setupControl(true, true, false);
}
// Build a minimal unsigned Payment STObject suitable for sfExportedTxn.
static STObject
buildExportedPayment(
@@ -613,6 +620,64 @@ struct Export_test : public beast::unit_test::suite
env.close();
}
void
testExportNetworkRetryWithoutQuorum(FeatureBitset features)
{
testcase("ttEXPORT network mode retries without quorum");
using namespace jtx;
Env env{*this, exportTestConfig(), features};
Account const alice{"alice"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, carol);
env.close();
forceNonStandalone(env.app());
BEAST_EXPECT(!env.app().config().standalone());
ConfigVals cfg;
cfg.noExportSig = true;
env.app().getRuntimeConfig().setConfig("*", cfg);
auto const seq = env.current()->seq();
auto const ticketSeq = std::uint32_t{1};
auto const lls = seq + 5;
auto innerObj = buildExportedPayment(
alice.id(), carol.id(), seq + 1, lls, ticketSeq);
Json::Value jv;
jv[jss::TransactionType] = jss::Export;
jv[jss::Account] = alice.human();
jv[jss::LastLedgerSequence] = lls;
jv[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none);
env(jv, fee(XRP(1)), ter(tesSUCCESS));
BEAST_EXPECT(env.close(
env.now() + std::chrono::seconds{5}, std::chrono::milliseconds{0}));
std::optional<TER> closedResult;
for (auto const& [stx, meta] : env.closed()->txs)
{
if (!stx || stx->getTxnType() != ttEXPORT ||
stx->getAccountID(sfAccount) != alice.id())
{
continue;
}
auto const& exported =
stx->peekAtField(sfExportedTxn).downcast<STObject>();
if (exported.getFieldU32(sfTicketSequence) != ticketSeq)
continue;
if (meta)
closedResult = TER::fromInt((*meta)[sfTransactionResult]);
}
BEAST_EXPECT(!closedResult);
BEAST_EXPECT(!env.le(keylet::shadowTicket(alice.id(), ticketSeq)));
}
void
testOpenLedgerExportLimit(FeatureBitset features)
{
@@ -1060,6 +1125,7 @@ struct Export_test : public beast::unit_test::suite
// ttEXPORT transactor tests
testExportTxnOpenLedger(allWithExport);
testExportNetworkRetryWithoutQuorum(allWithExport);
testOpenLedgerExportLimit(allWithExport);
testShadowTicketLimit(allWithExport);
testShadowTicketLifecycle(allWithExport);

View File

@@ -20,6 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <test/shamap/common.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/proof/LedgerProof.h>
#include <xrpld/app/proof/ProofBuilder.h>
@@ -152,6 +153,62 @@ struct XPOP_test : public beast::unit_test::suite
BEAST_EXPECT(proofJson[3].asString() == to_string(manual.leafHash));
}
void
testProofBuilderSyntheticTrie()
{
testcase("ProofBuilder synthetic trie collisions");
tests::TestNodeFamily f{beast::Journal{beast::Journal::getNullSink()}};
SHAMap map{SHAMapType::TRANSACTION, f};
auto const keyA = uint256{
"1000000000000000000000000000000000000000000000000000000000000001"};
auto const keyB = uint256{
"1800000000000000000000000000000000000000000000000000000000000002"};
auto const keyC = uint256{
"2000000000000000000000000000000000000000000000000000000000000003"};
auto const keyD = uint256{
"1f00000000000000000000000000000000000000000000000000000000000004"};
auto add = [&](uint256 const& key, Blob data) {
return map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(key, makeSlice(data)));
};
auto payload = [](std::uint8_t first) {
Blob data;
data.reserve(12);
for (std::uint8_t i = 0; i < 12; ++i)
data.push_back(first + i);
return data;
};
BEAST_EXPECT(add(keyA, payload(0x01)));
BEAST_EXPECT(add(keyB, payload(0x11)));
BEAST_EXPECT(add(keyC, payload(0x21)));
BEAST_EXPECT(add(keyD, payload(0x31)));
map.invariants();
auto const proof = proof::extractProofV1(map, keyA);
BEAST_EXPECT(proof.has_value());
if (proof)
{
BEAST_EXPECT(proof->path.size() == 2);
auto const computedRoot = proof->computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(proof->verify(*computedRoot));
auto const json = proof->toJsonV1();
BEAST_EXPECT(json.isArray());
BEAST_EXPECT(json[proof->path.front().targetBranch].isArray());
}
auto const nearMiss = uint256{
"10000000000000000000000000000000000000000000000000000000000000ff"};
BEAST_EXPECT(!proof::extractProofV1(map, nearMiss));
}
void
testBuildXPOPv1()
{
@@ -385,6 +442,7 @@ struct XPOP_test : public beast::unit_test::suite
{
testBuildLedgerProof();
testProofBuilderEdgeCases();
testProofBuilderSyntheticTrie();
testBuildXPOPv1();
testBuildXPOPv1WithoutMerkleProof();
testMerkleProofVerification();

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,13 @@ public:
{
}
static void
enableSilentTracing(csf::Sim& sim)
{
sim.sink.silent(true);
sim.sink.threshold(beast::severities::kTrace);
}
void
testShouldCloseLedger()
{
@@ -214,6 +221,7 @@ public:
//@@start peers-agree
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(5);
// Connected trust and network graphs with single fixed delay
@@ -260,6 +268,7 @@ public:
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(1);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -318,6 +327,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(2);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -447,6 +457,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup groupA = sim.createGroup(2);
PeerGroup groupB = sim.createGroup(2);
@@ -480,6 +491,52 @@ public:
}
}
void
testBootstrapFastStart()
{
using namespace csf;
using namespace std::chrono;
testcase("bootstrap fast start");
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(4);
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
for (Peer* peer : peers)
{
peer->ce().bootstrapFastStartEnabled_ = true;
peer->targetLedgers =
static_cast<int>(parms.bootstrapStableRoundsRequired);
peer->start();
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(json["bootstrap_fast_start"].asBool());
BEAST_EXPECT(
json["previous_mseconds"].asInt() ==
parms.bootstrapRoundTimeSeed.count());
BEAST_EXPECT(json["bootstrap_stable_rounds"].asInt() == 0);
}
sim.scheduler.step();
if (BEAST_EXPECT(sim.synchronized()))
{
for (Peer* peer : peers)
{
BEAST_EXPECT(
peer->completedLedgers ==
static_cast<int>(parms.bootstrapStableRoundsRequired));
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(!json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(peer->prevRoundTime < parms.ledgerIDLE_INTERVAL);
}
}
}
void
testWrongLCL()
{
@@ -520,6 +577,7 @@ public:
//@@end wrong-lcl-scenario
Sim sim;
enableSilentTracing(sim);
PeerGroup minority = sim.createGroup(2);
PeerGroup majorityA = sim.createGroup(3);
@@ -624,6 +682,7 @@ public:
// after it is already in the establish phase of the next round.
Sim sim;
enableSilentTracing(sim);
PeerGroup loner = sim.createGroup(1);
PeerGroup friends = sim.createGroup(3);
loner.trust(loner + friends);
@@ -669,6 +728,7 @@ public:
ConsensusParms parms;
Sim sim;
enableSilentTracing(sim);
// This requires a group of 4 fast and 2 slow peers to create a
// situation in which a subset of peers requires seeing additional
@@ -770,6 +830,7 @@ public:
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
std::uint32_t numA = (numPeers - overlap) / 2;
std::uint32_t numB = numPeers - numA - overlap;
@@ -828,6 +889,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup validators = sim.createGroup(5);
PeerGroup center = sim.createGroup(1);
validators.trust(validators);
@@ -943,6 +1005,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
// Goes A->B->D
PeerGroup groupABD = sim.createGroup(2);
@@ -1066,6 +1129,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
SimDuration delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup behind = sim.createGroup(3);
@@ -1542,6 +1606,7 @@ public:
testPeersAgree();
testSlowPeers();
testCloseTimeDisagree();
testBootstrapFastStart();
testWrongLCL();
testConsensusCloseTimeRounding();
testFork();

View File

@@ -367,6 +367,9 @@ struct Peer
// Optional test hook: stay an active proposer but do not originate an
// export signature, so tests can force sidecar-fetch-only convergence.
bool suppressOwnExportSig_ = false;
// Optional test hook: exercise generic Consensus bootstrap timing
// without making the CSF runtime-config aware.
bool bootstrapFastStartEnabled_ = false;
explicit Extensions(Peer& p) : peer(p), j_(p.j)
{
@@ -887,7 +890,7 @@ struct Peer
bool
bootstrapFastStartEnabled() const
{
return false;
return bootstrapFastStartEnabled_;
}
bool
shouldSendExplicitFinalProposal() const

View File

@@ -42,6 +42,7 @@ namespace csf {
class BasicSink : public beast::Journal::Sink
{
Scheduler::clock_type const& clock_;
bool silent_ = false;
public:
BasicSink(Scheduler::clock_type const& clock)
@@ -49,11 +50,19 @@ public:
{
}
void
silent(bool value)
{
silent_ = value;
}
void
write(beast::severities::Severity level, std::string const& text) override
{
if (level < threshold())
return;
if (silent_)
return;
std::cout << clock_.now().time_since_epoch().count() << " " << text
<< std::endl;

View File

@@ -0,0 +1,232 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/overlay/detail/ProposalPrecheck.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <string_view>
#include <vector>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
void
setPreviousLedger(protocol::TMProposeSet& set)
{
auto const prev = makeHash("proposal-precheck-prev");
set.set_previousledger(prev.data(), prev.size());
}
void
setPosition(protocol::TMProposeSet& set, ExtendedPosition const& position)
{
Serializer s;
position.add(s);
set.set_currenttxhash(s.data(), s.size());
}
} // namespace
class ProposalPrecheck_test : public beast::unit_test::suite
{
public:
void
run() override
{
using enum detail::ProposalPrecheckResult;
testcase("legacy and extended ok");
{
protocol::TMProposeSet set;
setPreviousLedger(set);
ExtendedPosition position{makeHash("legacy-position")};
setPosition(set, position);
auto const precheck =
detail::checkProposalExtensions(set, false, false);
BEAST_EXPECT(precheck.result == ok);
BEAST_EXPECT(precheck.position);
if (precheck.position)
BEAST_EXPECT(precheck.position->txSetHash == position.txSetHash);
}
testcase("malformed hashes and extended payload");
{
protocol::TMProposeSet set;
setPreviousLedger(set);
set.set_currenttxhash("short", 5);
BEAST_EXPECT(
detail::checkProposalExtensions(set, true, true).result ==
badHashes);
set.clear_currenttxhash();
std::string malformed(uint256::size(), '\0');
malformed.push_back(static_cast<char>(0x80));
set.set_currenttxhash(malformed.data(), malformed.size());
BEAST_EXPECT(
detail::checkProposalExtensions(set, true, true).result ==
badPosition);
protocol::TMProposeSet badPrev;
ExtendedPosition position{makeHash("bad-prev-position")};
setPosition(badPrev, position);
badPrev.set_previousledger("short", 5);
BEAST_EXPECT(
detail::checkProposalExtensions(badPrev, true, true).result ==
badHashes);
}
testcase("feature gating");
{
protocol::TMProposeSet entropySet;
setPreviousLedger(entropySet);
ExtendedPosition entropy{makeHash("entropy-position")};
entropy.myCommitment = makeHash("commitment");
setPosition(entropySet, entropy);
BEAST_EXPECT(
detail::checkProposalExtensions(entropySet, false, true)
.result == entropyDisabled);
protocol::TMProposeSet exportSet;
setPreviousLedger(exportSet);
ExtendedPosition exportPos{makeHash("export-position")};
exportPos.exportSigSetHash = makeHash("export-sidecar");
setPosition(exportSet, exportPos);
BEAST_EXPECT(
detail::checkProposalExtensions(exportSet, true, false)
.result == exportDisabled);
}
testcase("export signature binding");
{
protocol::TMProposeSet tooMany;
setPreviousLedger(tooMany);
ExtendedPosition position{makeHash("too-many-position")};
setPosition(tooMany, position);
for (std::uint8_t i = 0; i <= ExportLimits::maxPendingExports; ++i)
tooMany.add_exportsignatures("sig");
BEAST_EXPECT(
detail::checkProposalExtensions(tooMany, true, true).result ==
tooManyExportSignatures);
protocol::TMProposeSet unsignedSigs;
setPreviousLedger(unsignedSigs);
setPosition(unsignedSigs, position);
unsignedSigs.add_exportsignatures("sig");
BEAST_EXPECT(
detail::checkProposalExtensions(unsignedSigs, true, true)
.result == unsignedExportSignatures);
protocol::TMProposeSet mismatch;
setPreviousLedger(mismatch);
ExtendedPosition mismatchPos{makeHash("mismatch-position")};
mismatchPos.exportSignaturesHash =
proposalExportSignaturesHash(std::vector<std::string>{"one"});
setPosition(mismatch, mismatchPos);
mismatch.add_exportsignatures("two");
BEAST_EXPECT(
detail::checkProposalExtensions(mismatch, true, true).result ==
exportSignaturesHashMismatch);
protocol::TMProposeSet missing;
setPreviousLedger(missing);
setPosition(missing, mismatchPos);
BEAST_EXPECT(
detail::checkProposalExtensions(missing, true, true).result ==
missingExportSignatures);
protocol::TMProposeSet okSet;
setPreviousLedger(okSet);
std::vector<std::string> const sigs{"signed-export"};
ExtendedPosition okPos{makeHash("export-ok-position")};
okPos.exportSignaturesHash = proposalExportSignaturesHash(sigs);
setPosition(okSet, okPos);
okSet.add_exportsignatures(sigs.front());
BEAST_EXPECT(
detail::checkProposalExtensions(okSet, true, true).result ==
ok);
}
testcase("rejection diagnostics");
{
BEAST_EXPECT(!detail::proposalPrecheckRejection(ok));
auto const check = [&](detail::ProposalPrecheckResult result,
char const* logMessage,
char const* feeReason) {
auto const rejection = detail::proposalPrecheckRejection(result);
BEAST_EXPECT(rejection);
if (rejection)
{
BEAST_EXPECT(
std::string_view{rejection->logMessage} ==
logMessage);
BEAST_EXPECT(
std::string_view{rejection->feeReason} == feeReason);
}
};
check(badHashes, "Proposal: malformed", "bad hashes");
check(
badPosition,
"Proposal: malformed extended position",
"bad proposal position");
check(
entropyDisabled,
"Proposal: entropy fields while featureConsensusEntropy disabled",
"entropy fields disabled");
check(
exportDisabled,
"Proposal: export fields while featureExport disabled",
"export fields disabled");
check(
tooManyExportSignatures,
"Proposal: too many export signatures",
"too many export sigs");
check(
unsignedExportSignatures,
"Proposal: unsigned export signatures",
"unsigned export sigs");
check(
exportSignaturesHashMismatch,
"Proposal: export signatures hash mismatch",
"export sig hash mismatch");
check(
missingExportSignatures,
"Proposal: missing signed export signatures",
"missing export sigs");
BEAST_EXPECT(!detail::proposalPrecheckRejection(
static_cast<detail::ProposalPrecheckResult>(255)));
}
}
};
BEAST_DEFINE_TESTSUITE(ProposalPrecheck, overlay, ripple);
} // namespace test
} // namespace ripple

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <test/jtx.h>
#include <xrpld/core/Config.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
@@ -46,11 +47,64 @@ class Connect_test : public beast::unit_test::suite
}
}
void
testDisconnect()
{
testcase("Disconnect");
using namespace test::jtx;
Env env{*this};
const_cast<Config&>(env.app().config()).setupControl(true, true, false);
BEAST_EXPECT(!env.app().config().standalone());
{
auto const result = env.rpc("json", "disconnect", "{}");
BEAST_EXPECT(result[jss::result][jss::status] == "error");
BEAST_EXPECT(result[jss::result].isMember(jss::error));
BEAST_EXPECT(
result[jss::result][jss::error_message]
.asString()
.find("ip") != std::string::npos);
}
{
auto const result = env.rpc(
"json",
"disconnect",
R"({"ip":"127.0.0.1","port":"bad"})");
BEAST_EXPECT(result[jss::result][jss::status] == "error");
BEAST_EXPECT(result[jss::result][jss::error] == "invalidParams");
}
{
auto const result =
env.rpc("json", "disconnect", R"({"ip":"0.0.0.0"})");
BEAST_EXPECT(result[jss::result][jss::status] == "success");
BEAST_EXPECT(
result[jss::result][jss::message].asString().find(
"port: 21337") != std::string::npos);
BEAST_EXPECT(
result[jss::result][jss::message].asString().find(
"peers: 0") != std::string::npos);
}
{
auto const result = env.rpc(
"json", "disconnect", R"({"ip":"127.0.0.1","port":6000})");
BEAST_EXPECT(result[jss::result][jss::status] == "success");
BEAST_EXPECT(
result[jss::result][jss::message].asString().find(
"port: 6000") != std::string::npos);
}
}
public:
void
run() override
{
testErrors();
testDisconnect();
}
};

View File

@@ -850,6 +850,38 @@ class RuntimeConfig_test : public beast::unit_test::suite
BEAST_EXPECT(cfg->rngClaimDropPctX100 == 0); // clamped to 0%
}
void
testRngAndExportRuntimeToggles()
{
testcase("rng/export runtime toggles round-trip");
using namespace test::jtx;
Env env{*this};
Json::Value params;
params["set"] = Json::objectValue;
params["set"]["*"] = Json::objectValue;
params["set"]["*"]["bootstrap_fast_start"] = false;
params["set"]["*"]["rng_poll_ms"] = 5;
params["set"]["*"]["no_export_sig"] = true;
auto const result = runtimeConfig(env, params);
auto const& global = result["configs"]["*"];
BEAST_EXPECT(global["bootstrap_fast_start"].asBool() == false);
BEAST_EXPECT(global["rng_poll_ms"].asInt() == 50);
BEAST_EXPECT(global["no_export_sig"].asBool() == true);
auto const cfg = env.app().getRuntimeConfig().getConfig("*");
BEAST_EXPECT(cfg.has_value());
if (cfg)
{
BEAST_EXPECT(cfg->bootstrapFastStart.has_value());
BEAST_EXPECT(*cfg->bootstrapFastStart == false);
BEAST_EXPECT(cfg->rngPollMs == 50);
BEAST_EXPECT(cfg->noExportSig.has_value());
BEAST_EXPECT(*cfg->noExportSig == true);
}
}
void
testExplicitFinalProposalToggle()
{
@@ -968,6 +1000,7 @@ public:
testDropPctClamping();
testRngClaimDropPct();
testRngClaimDropPctClamping();
testRngAndExportRuntimeToggles();
testExplicitFinalProposalToggle();
testPerPeerClearInheritedFilter();
}

View File

@@ -20,10 +20,12 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapSidecarLeafNode.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/HashPrefix.h>
namespace ripple {
namespace tests {
@@ -122,6 +124,65 @@ public:
run(true, journal);
run(false, journal);
testSidecarLeaf(journal);
}
void
testSidecarLeaf(beast::Journal const& journal)
{
testcase("sidecar leaf");
Blob const payload{
0x53, 0x49, 0x44, 0x45, 0x43, 0x41, 0x52, 0x00, 0x01, 0x02,
0x03, 0x04};
auto const itemHash = sha512Half(HashPrefix::sidecar, makeSlice(payload));
auto const item = make_shamapitem(itemHash, makeSlice(payload));
SHAMapSidecarLeafNode leaf{item, 7};
BEAST_EXPECT(leaf.cowid() == 7);
BEAST_EXPECT(leaf.getType() == SHAMapNodeType::tnSIDECAR);
BEAST_EXPECT(leaf.getHash() == SHAMapHash{itemHash});
BEAST_EXPECT(leaf.peekItem()->slice() == makeSlice(payload));
auto const label = leaf.getString(SHAMapNodeID{});
BEAST_EXPECT(label.find(",sidecar") != std::string::npos);
auto const cloned = leaf.clone(11);
BEAST_EXPECT(cloned->cowid() == 11);
BEAST_EXPECT(cloned->getType() == SHAMapNodeType::tnSIDECAR);
BEAST_EXPECT(cloned->getHash() == leaf.getHash());
Serializer wire;
leaf.serializeForWire(wire);
auto const wireSlice = wire.slice();
auto const wirePayload = Slice{wireSlice.data(), payload.size()};
BEAST_EXPECT(wireSlice.size() == payload.size() + 1);
BEAST_EXPECT(wirePayload == makeSlice(payload));
BEAST_EXPECT(wireSlice[wireSlice.size() - 1] == wireTypeSidecar);
auto const fromWire = SHAMapTreeNode::makeFromWire(wireSlice);
BEAST_EXPECT(fromWire->getType() == SHAMapNodeType::tnSIDECAR);
BEAST_EXPECT(fromWire->getHash() == leaf.getHash());
Serializer prefixed;
leaf.serializeWithPrefix(prefixed);
auto const fromPrefix =
SHAMapTreeNode::makeFromPrefix(prefixed.slice(), leaf.getHash());
BEAST_EXPECT(fromPrefix->getType() == SHAMapNodeType::tnSIDECAR);
BEAST_EXPECT(fromPrefix->getHash() == leaf.getHash());
tests::TestNodeFamily f(journal);
SHAMap sidecars{SHAMapType::SIDECAR, f};
BEAST_EXPECT(sidecars.addItem(
SHAMapNodeType::tnSIDECAR, make_shamapitem(*item)));
sidecars.invariants();
BEAST_EXPECT(sidecars.hasItem(itemHash));
BEAST_EXPECT(sidecars.peekItem(itemHash)->slice() == makeSlice(payload));
SHAMapMissingNode missing{SHAMapType::SIDECAR, leaf.getHash()};
BEAST_EXPECT(
std::string{missing.what()}.find("Sidecar Tree") !=
std::string::npos);
}
void

View File

@@ -146,6 +146,10 @@ harvestExportSignatures(
continue;
auto const fullSlice = makeSlice(blob);
auto const pkSlice = fullSlice.substr(32, 33);
if (!publicKeyType(pkSlice))
continue;
auto const sigSlice = fullSlice.substr(65);
auto const txIt = input.exportTxns.find(txHash);

View File

@@ -33,6 +33,7 @@
#include <xrpld/app/tx/apply.h>
#include <xrpld/overlay/Cluster.h>
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpld/overlay/detail/ProposalPrecheck.h>
#include <xrpld/overlay/detail/Tuning.h>
#include <xrpld/perflog/PerfLog.h>
#include <xrpl/basics/UptimeClock.h>
@@ -40,7 +41,6 @@
#include <xrpl/basics/random.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/core/LexicalCast.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/digest.h>
@@ -1721,14 +1721,6 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
return;
}
if (set.currenttxhash().size() < uint256::size() ||
!stringIsUint256Sized(set.previousledger()))
{
JLOG(p_journal_.warn()) << "Proposal: malformed";
fee_.update(Resource::feeMalformedRequest, "bad hashes");
return;
}
// RH TODO: when isTrusted = false we should probably also cache a key
// suppression for 30 seconds to avoid doing a relatively expensive lookup
// every time a spam packet is received
@@ -1741,85 +1733,26 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
if (!isTrusted && app_.config().RELAY_UNTRUSTED_PROPOSALS == -1)
return;
auto const currentPosSlice = makeSlice(set.currenttxhash());
SerialIter currentPosSit{currentPosSlice};
auto const parsedPosition = ExtendedPosition::fromSerialIter(
currentPosSit, set.currenttxhash().size());
if (!parsedPosition)
auto const openLedger = app_.openLedger().current();
auto const precheck = detail::checkProposalExtensions(
set,
openLedger && openLedger->rules().enabled(featureConsensusEntropy),
openLedger && openLedger->rules().enabled(featureExport));
if (auto const rejection =
detail::proposalPrecheckRejection(precheck.result))
{
JLOG(p_journal_.warn()) << "Proposal: malformed extended position";
fee_.update(Resource::feeMalformedRequest, "bad proposal position");
return;
}
bool const hasEntropyMaterial = parsedPosition->commitSetHash ||
parsedPosition->entropySetHash || parsedPosition->myCommitment ||
parsedPosition->myReveal;
bool const hasExportMaterial = parsedPosition->exportSigSetHash ||
parsedPosition->exportSignaturesHash || set.exportsignatures_size() > 0;
if (hasEntropyMaterial || hasExportMaterial)
{
auto const openLedger = app_.openLedger().current();
bool const entropyEnabled =
openLedger && openLedger->rules().enabled(featureConsensusEntropy);
bool const exportEnabled =
openLedger && openLedger->rules().enabled(featureExport);
if (hasEntropyMaterial && !entropyEnabled)
{
JLOG(p_journal_.warn())
<< "Proposal: entropy fields while featureConsensusEntropy "
"disabled";
fee_.update(
Resource::feeMalformedRequest, "entropy fields disabled");
return;
}
if (hasExportMaterial && !exportEnabled)
{
JLOG(p_journal_.warn())
<< "Proposal: export fields while featureExport disabled";
fee_.update(
Resource::feeMalformedRequest, "export fields disabled");
return;
}
}
if (set.exportsignatures_size() > ExportLimits::maxPendingExports)
{
JLOG(p_journal_.warn()) << "Proposal: too many export signatures";
fee_.update(Resource::feeMalformedRequest, "too many export sigs");
return;
}
if (set.exportsignatures_size() > 0)
{
if (!parsedPosition->exportSignaturesHash)
{
JLOG(p_journal_.warn()) << "Proposal: unsigned export signatures";
fee_.update(Resource::feeMalformedRequest, "unsigned export sigs");
return;
}
if (proposalExportSignaturesHash(set.exportsignatures()) !=
*parsedPosition->exportSignaturesHash)
{
JLOG(p_journal_.warn())
<< "Proposal: export signatures hash mismatch";
fee_.update(
Resource::feeMalformedRequest, "export sig hash mismatch");
return;
}
}
else if (parsedPosition->exportSignaturesHash)
{
JLOG(p_journal_.warn()) << "Proposal: missing signed export signatures";
fee_.update(Resource::feeMalformedRequest, "missing export sigs");
JLOG(p_journal_.warn()) << rejection->logMessage;
fee_.update(Resource::feeMalformedRequest, rejection->feeReason);
return;
}
auto const& parsedPosition = *precheck.position;
uint256 const prevLedger{set.previousledger()};
NetClock::time_point const closeTime{NetClock::duration{set.closetime()}};
uint256 const suppression = proposalUniqueId(
*parsedPosition,
parsedPosition,
prevLedger,
set.proposeseq(),
closeTime,
@@ -1876,7 +1809,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
RCLCxPeerPos::Proposal{
prevLedger,
set.proposeseq(),
*parsedPosition,
parsedPosition,
closeTime,
app_.timeKeeper().closeTime(),
calcNodeID(app_.validatorManifests().getMasterKey(publicKey))},

View File

@@ -0,0 +1,138 @@
#ifndef RIPPLE_OVERLAY_DETAIL_PROPOSALPRECHECK_H_INCLUDED
#define RIPPLE_OVERLAY_DETAIL_PROPOSALPRECHECK_H_INCLUDED
#include <xrpld/app/consensus/RCLCxPeerPos.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/messages.h>
#include <optional>
namespace ripple {
namespace detail {
enum class ProposalPrecheckResult {
ok,
badHashes,
badPosition,
entropyDisabled,
exportDisabled,
tooManyExportSignatures,
unsignedExportSignatures,
exportSignaturesHashMismatch,
missingExportSignatures
};
struct ProposalPrecheck
{
ProposalPrecheckResult result = ProposalPrecheckResult::ok;
std::optional<ExtendedPosition> position;
};
struct ProposalPrecheckRejection
{
char const* logMessage;
char const* feeReason;
};
inline std::optional<ProposalPrecheckRejection>
proposalPrecheckRejection(ProposalPrecheckResult result)
{
switch (result)
{
case ProposalPrecheckResult::ok:
return std::nullopt;
case ProposalPrecheckResult::badHashes:
return ProposalPrecheckRejection{
"Proposal: malformed", "bad hashes"};
case ProposalPrecheckResult::badPosition:
return ProposalPrecheckRejection{
"Proposal: malformed extended position",
"bad proposal position"};
case ProposalPrecheckResult::entropyDisabled:
return ProposalPrecheckRejection{
"Proposal: entropy fields while featureConsensusEntropy disabled",
"entropy fields disabled"};
case ProposalPrecheckResult::exportDisabled:
return ProposalPrecheckRejection{
"Proposal: export fields while featureExport disabled",
"export fields disabled"};
case ProposalPrecheckResult::tooManyExportSignatures:
return ProposalPrecheckRejection{
"Proposal: too many export signatures", "too many export sigs"};
case ProposalPrecheckResult::unsignedExportSignatures:
return ProposalPrecheckRejection{
"Proposal: unsigned export signatures", "unsigned export sigs"};
case ProposalPrecheckResult::exportSignaturesHashMismatch:
return ProposalPrecheckRejection{
"Proposal: export signatures hash mismatch",
"export sig hash mismatch"};
case ProposalPrecheckResult::missingExportSignatures:
return ProposalPrecheckRejection{
"Proposal: missing signed export signatures",
"missing export sigs"};
}
return std::nullopt;
}
inline ProposalPrecheck
checkProposalExtensions(
protocol::TMProposeSet const& set,
bool entropyEnabled,
bool exportEnabled)
{
if (set.currenttxhash().size() < uint256::size() ||
set.previousledger().size() != uint256::size())
{
return {ProposalPrecheckResult::badHashes, std::nullopt};
}
auto const currentPosSlice = makeSlice(set.currenttxhash());
SerialIter currentPosSit{currentPosSlice};
auto const parsedPosition = ExtendedPosition::fromSerialIter(
currentPosSit, set.currenttxhash().size());
if (!parsedPosition)
return {ProposalPrecheckResult::badPosition, std::nullopt};
bool const hasEntropyMaterial = parsedPosition->commitSetHash ||
parsedPosition->entropySetHash || parsedPosition->myCommitment ||
parsedPosition->myReveal;
bool const hasExportMaterial = parsedPosition->exportSigSetHash ||
parsedPosition->exportSignaturesHash || set.exportsignatures_size() > 0;
if (hasEntropyMaterial && !entropyEnabled)
return {ProposalPrecheckResult::entropyDisabled, parsedPosition};
if (hasExportMaterial && !exportEnabled)
return {ProposalPrecheckResult::exportDisabled, parsedPosition};
if (set.exportsignatures_size() > ExportLimits::maxPendingExports)
return {
ProposalPrecheckResult::tooManyExportSignatures, parsedPosition};
if (set.exportsignatures_size() > 0)
{
if (!parsedPosition->exportSignaturesHash)
return {
ProposalPrecheckResult::unsignedExportSignatures,
parsedPosition};
if (proposalExportSignaturesHash(set.exportsignatures()) !=
*parsedPosition->exportSignaturesHash)
{
return {
ProposalPrecheckResult::exportSignaturesHashMismatch,
parsedPosition};
}
}
else if (parsedPosition->exportSignaturesHash)
{
return {
ProposalPrecheckResult::missingExportSignatures, parsedPosition};
}
return {ProposalPrecheckResult::ok, parsedPosition};
}
} // namespace detail
} // namespace ripple
#endif