Compare commits

..

12 Commits

Author SHA1 Message Date
Ed Hennis
923a822491 Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  refactor: Clean up tec object deletion logic (6588)
  fix: Move AMMInvariant weakInvariantCheck logic into the transaction (7032)
  fix: Improve ValidAMM invariant (7295)
  feat: Remove clear mutable flags for DynamicMPT XLS-94 (7439)
  ci: Build and push docker images in forks too (7588)
  ci: [DEPENDABOT] bump actions/checkout from 6.0.3 to 7.0.0 (7585)
  fix: Ensure xrpld service directories exist at startup (7565)
  fix: Use template for granular delegation permissions (6613)
2026-06-22 18:15:16 -04:00
Ed Hennis
9ab4f325ce test: Check some anomalous results in CI 2026-06-22 18:13:46 -04:00
Ed Hennis
05ce347036 Explicitly initialize base_uint.data_ 2026-06-22 18:13:45 -04:00
Ed Hennis
f00c07da2f Debug the testFromRawSizeMismatch test in CI 2026-06-22 18:13:45 -04:00
Ed Hennis
850dba46fb Update levelization 2026-06-22 18:13:45 -04:00
Ed Hennis
2853c64974 clang-tidy: includes and variable names 2026-06-22 18:13:44 -04:00
Ed Hennis
dd95b326b3 Update names, etc. to match develop 2026-06-22 18:13:44 -04:00
Ed Hennis
f2a22d4b99 Fix formatting 2026-06-22 18:13:44 -04:00
Ed Hennis
0fc1c735a1 Add back tests introduced by #29 #57 2026-06-22 18:13:44 -04:00
Ed Hennis
ea7a7de5de Add back tests introduced by #64 2026-06-22 18:13:43 -04:00
Ed Hennis
62bce4e178 test: Add tests for #46 2026-06-22 18:13:43 -04:00
Sergey Kuznetsov
3086504658 tests: Tests for fix in account objects rpc (#58) (#59) 2026-06-22 18:13:43 -04:00
9 changed files with 753 additions and 213 deletions

View File

@@ -161,6 +161,7 @@ test.peerfinder > xrpl.protocol
test.protocol > test.jtx
test.protocol > test.unit_test
test.protocol > xrpl.basics
test.protocol > xrpld.core
test.protocol > xrpl.json
test.protocol > xrpl.protocol
test.resource > test.unit_test

View File

@@ -939,6 +939,46 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply));
{
// bad reply: invalid hash/key sizes
{
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with undersized key (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with oversized key (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
{
// reply with empty key
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
}
}
{
// bad reply
// bad header
@@ -988,6 +1028,28 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply));
{
// bad reply: invalid hash sizes
{
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
}
}
{
// bad reply
// bad header

View File

@@ -8,21 +8,18 @@
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/txflags.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <optional>
@@ -529,47 +526,6 @@ class PermissionedDomains_test : public beast::unit_test::Suite
BEAST_EXPECT(env.ownerCount(alice) == 1);
}
void
testTicket(FeatureBitset features)
{
testcase("Tickets");
using namespace test::jtx;
Env env(*this, features);
Account const alice("alice");
env.fund(XRP(1000), alice);
pdomain::Credentials const credentials{
{.issuer = alice, .credType = "credential1"},
};
std::uint32_t seq{env.seq(alice)};
env(ticket::create(alice, 2));
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq));
auto domain = pdomain::getNewDomain(env.meta());
if (features[fixCleanup3_1_3])
{
BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), seq).key);
}
else
{
BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), 0).key);
}
}
if (features[fixCleanup3_1_3])
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq));
}
else
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq), Ter(tefEXCEPTION));
}
}
public:
void
run() override
@@ -584,8 +540,6 @@ public:
testDelete(withFix_);
testAccountReserve(withFeature_);
testAccountReserve(withFix_);
testTicket(withFeature_);
testTicket(withFix_);
}
};

View File

@@ -117,6 +117,34 @@ struct base_uint_test : beast::unit_test::Suite
}
}
#ifdef NDEBUG
void
testFromRawSizeMismatch()
{
testcase("base_uint: fromRaw size mismatch");
// Container smaller than the base_uint (8 bytes vs 12 bytes for
// test96). Only the first 8 bytes are copied; the remaining 4 bytes
// stay zero.
{
Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8};
test96 const result = test96::fromRaw(tooSmall);
auto const resultText = to_string(result);
BEAST_EXPECTS(resultText.substr(0, 16) == "0102030405060708", resultText);
}
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
{
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
test96 const result = test96::fromRaw(tooBig);
auto const resultText = to_string(result);
BEAST_EXPECTS(resultText == "0102030405060708090A0B0C", resultText);
}
}
#endif
void
run() override
{
@@ -125,6 +153,10 @@ struct base_uint_test : beast::unit_test::Suite
static_assert(!std::is_constructible_v<test96, std::complex<double>>);
static_assert(!std::is_assignable_v<test96&, std::complex<double>>);
#ifdef NDEBUG
testFromRawSizeMismatch();
#endif
testComparisons();
// used to verify set insertion (hashing required)
@@ -194,6 +226,19 @@ struct base_uint_test : beast::unit_test::Suite
BEAST_EXPECT(d == 0);
}
{
// There are several ways to create a zero. beast::kZero is tested above. Test some
// others.
test96 z1;
BEAST_EXPECTS(z1 == z, to_string(z1));
test96 z2{};
BEAST_EXPECTS(z2 == z, to_string(z2));
test96 z3{0u};
BEAST_EXPECTS(z3 == z, to_string(z2));
}
test96 n{z};
n++;
BEAST_EXPECT(n == test96(1));

View File

@@ -1,10 +1,12 @@
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <functional>
#include <map>
@@ -863,6 +865,101 @@ public:
//--------------------------------------------------------------------------
void
testIssueFromJson()
{
testcase("issueFromJson");
// Valid XRP — no issuer field
{
json::Value jv;
jv[jss::currency] = "XRP";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(isXRP(issue));
}
// Valid IOU — legitimate issuer
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(!isXRP(issue));
BEAST_EXPECT(issue.account != noAccount());
}
// noAccount() is the MPT sentinel in binary serialization - must be
// rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
issueFromJson(jv);
fail("noAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// xrpAccount() is the XRP sentinel (all zeros) - must be rejected
// as IOU issuer
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
issueFromJson(jv);
fail("xrpAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Invalid base58 — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "not_a_valid_address";
issueFromJson(jv);
fail("invalid base58 accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Non-XRP currency with no issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
issueFromJson(jv);
fail("missing issuer accepted");
}
catch (...)
{
pass();
}
// XRP with an issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "XRP";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
issueFromJson(jv);
fail("XRP with issuer accepted");
}
catch (...)
{
pass();
}
}
void
run() override
{
@@ -897,6 +994,9 @@ public:
// ---
testIssueDomainSets();
testIssueDomainMaps();
// ---
testIssueFromJson();
}
};

View File

@@ -1,15 +1,24 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h> // IWYU pragma: keep
#include <test/jtx/envconfig.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <memory>
namespace xrpl::test {
@@ -137,12 +146,143 @@ public:
"000000000000000000000000000000000000000000000002");
}
void
testNoAccountIssuerRpc()
{
testcase("noAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(noAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::error));
}
void
testNoAccountIssuer()
{
testcase("noAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted noAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
{
Serializer s;
s.addBitString(toCurrency("USD"));
s.addBitString(noAccount());
SerialIter iter(s.slice());
try
{
STIssue const stissue(iter, sfAsset);
fail(
"STIssue deserialization of [USD][noAccount()] should "
"throw");
}
catch (...)
{
pass();
}
}
}
void
testXrpAccountIssuerRpc()
{
testcase("xrpAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(xrpAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(result.isMember(jss::error));
}
void
testXrpAccountIssuer()
{
testcase("xrpAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted xrpAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
}
void
run() override
{
// compliments other unit tests to ensure complete coverage
testConstructor();
testCompare();
testNoAccountIssuerRpc();
testNoAccountIssuer();
testXrpAccountIssuerRpc();
testXrpAccountIssuer();
}
};

View File

@@ -23,6 +23,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nft.h>
@@ -32,6 +33,7 @@
#include <cstdint>
#include <iterator>
#include <optional>
#include <ranges>
#include <vector>
namespace xrpl::test {
@@ -1350,6 +1352,86 @@ public:
}
}
void
testAccountObjectDoesntShowCancelledOffers()
{
testcase("AccountObjectDoesntShowCancelledOffers");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
auto const eur = bob["EUR"];
env.fund(XRP(10000), alice, bob);
env.close();
auto const rpcAccountObjects = [&](std::optional<uint32_t> limit = std::nullopt) {
json::Value params;
params[jss::account] = alice.human();
if (limit.has_value())
{
params[jss::limit] = *limit;
}
return env.rpc("json", "account_objects", to_string(params));
};
auto const numEntries = 33;
std::vector<uint32_t> seqs;
seqs.reserve(numEntries);
for ([[maybe_unused]] auto _ : std::ranges::iota_view{0, numEntries})
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offer(alice, eur(1), XRP(2));
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::status].asString() == "success");
seqs.push_back(env.seq(alice));
}
auto res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == numEntries);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
for (auto const s : std::views::all(seqs) | std::views::take(numEntries - 1))
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offerCancel(alice, s - 1);
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::status].asString() == "success");
}
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
json::Value txJson;
txJson[jss::TransactionType] = jss::NFTokenMint;
txJson[jss::Account] = to_string(alice.id());
txJson["NFTokenTaxon"] = 1;
params[jss::tx_json] = txJson;
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::status].asString() == "success");
}
env.close();
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 2);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
res = rpcAccountObjects(1);
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(res[jss::result][jss::limit].asUInt() == 1);
BEAST_EXPECT(res[jss::result].isMember(jss::marker));
}
void
run() override
{
@@ -1360,6 +1442,7 @@ public:
testNFTsMarker();
testAccountNFTs();
testAccountObjectMarker();
testAccountObjectDoesntShowCancelledOffers();
}
};

View File

@@ -2,6 +2,7 @@
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
@@ -881,6 +882,125 @@ class Transaction_test : public beast::unit_test::Suite
}
}
void
testSignForNetworkIDValidation()
{
testcase("SignFor NetworkID validation");
using namespace test::jtx;
Account const owner{"owner"};
Account const signer{"signer"};
auto makeConfig = [](std::uint32_t networkID) {
return envconfig([networkID](std::unique_ptr<Config> cfg) {
cfg->networkId = networkID;
return cfg;
});
};
auto setupEnv = [&](Env& env) {
env.fund(XRP(10'000), owner, signer);
env.close();
env(signers(owner, 1, {{signer, 1}}));
env.close();
};
auto makeTx = [&](Env& env) {
json::Value tx;
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::Account] = owner.human();
tx[jss::Sequence] = env.seq(owner);
tx[jss::Fee] = "100";
tx[jss::SigningPubKey] = "";
return tx;
};
auto signFor = [&](Env& env, json::Value const& tx) {
json::Value signReq;
signReq[jss::tx_json] = tx;
signReq[jss::account] = signer.human();
signReq[jss::secret] = signer.name();
return env.rpc("json", "sign_for", to_string(signReq))[jss::result];
};
// Test case: NetworkID < 1024 - field is not required
{
Env env{*this, makeConfig(500)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(!result[jss::tx_json].isMember(jss::NetworkID));
}
// Test case: NetworkID > 1024 - missing NetworkID field
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Missing field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not a number
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = "not_a_number";
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not integral
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040.1;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is different from
// actual NetworkID
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 9999;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is correct
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(result[jss::tx_json][jss::NetworkID].asUInt() == 2040);
}
}
public:
void
run() override
@@ -890,6 +1010,8 @@ public:
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
testSignForNetworkIDValidation();
}
void

View File

@@ -70,6 +70,7 @@
#include <boost/asio/completion_condition.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/write.hpp>
#include <boost/beast/core/multi_buffer.hpp>
@@ -197,70 +198,78 @@ stringIsUInt256Sized(std::string const& pBuffStr)
void
PeerImp::run()
{
dispatch(strand_, [self = shared_from_this()]() {
auto parseLedgerHash = [](std::string_view value) -> std::optional<uint256> {
if (uint256 ret; ret.parseHex(value))
return ret;
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::run, shared_from_this()));
return;
}
if (auto const s = base64Decode(value); s.size() == uint256::size())
return uint256::fromRaw(s);
auto parseLedgerHash = [](std::string_view value) -> std::optional<uint256> {
if (uint256 ret; ret.parseHex(value))
return ret;
return std::nullopt;
};
if (auto const s = base64Decode(value); s.size() == uint256::size())
return uint256::fromRaw(s);
std::optional<uint256> closed;
std::optional<uint256> previous;
return std::nullopt;
};
if (auto const iter = self->headers_.find("Closed-Ledger"); iter != self->headers_.end())
{
closed = parseLedgerHash(iter->value());
std::optional<uint256> closed;
std::optional<uint256> previous;
if (!closed)
self->fail("Malformed handshake data (1)");
}
if (auto const iter = headers_.find("Closed-Ledger"); iter != headers_.end())
{
closed = parseLedgerHash(iter->value());
if (auto const iter = self->headers_.find("Previous-Ledger"); iter != self->headers_.end())
{
previous = parseLedgerHash(iter->value());
if (!closed)
fail("Malformed handshake data (1)");
}
if (!previous)
self->fail("Malformed handshake data (2)");
}
if (auto const iter = headers_.find("Previous-Ledger"); iter != headers_.end())
{
previous = parseLedgerHash(iter->value());
if (previous && !closed)
self->fail("Malformed handshake data (3)");
if (!previous)
fail("Malformed handshake data (2)");
}
{
std::scoped_lock const sl(self->recentLock_);
if (closed)
self->closedLedgerHash_ = *closed;
if (previous)
self->previousLedgerHash_ = *previous;
}
if (previous && !closed)
fail("Malformed handshake data (3)");
if (self->inbound_)
{
self->doAccept();
}
else
{
self->doProtocolStart();
}
{
std::scoped_lock const sl(recentLock_);
if (closed)
closedLedgerHash_ = *closed;
if (previous)
previousLedgerHash_ = *previous;
}
// Anything else that needs to be done with the connection should be
// done in doProtocolStart
});
if (inbound_)
{
doAccept();
}
else
{
doProtocolStart();
}
// Anything else that needs to be done with the connection should be
// done in doProtocolStart
}
void
PeerImp::stop()
{
dispatch(strand_, [self = shared_from_this()]() {
if (!self->socket_.is_open())
return;
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::stop, shared_from_this()));
return;
}
self->close();
});
if (!socket_.is_open())
return;
close();
}
//------------------------------------------------------------------------------
@@ -268,111 +277,126 @@ PeerImp::stop()
void
PeerImp::send(std::shared_ptr<Message> const& m)
{
dispatch(strand_, [self = shared_from_this(), m]() {
if (self->gracefulClose_)
return;
if (self->detaching_)
return;
if (!self->socket_.is_open())
return;
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::send, shared_from_this(), m));
return;
}
if (gracefulClose_)
return;
if (detaching_)
return;
if (!socket_.is_open())
return;
auto validator = m->getValidatorKey();
if (validator && !self->squelch_.expireSquelch(*validator))
{
self->overlay_.reportOutboundTraffic(
TrafficCount::Category::SquelchSuppressed,
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
return;
}
auto validator = m->getValidatorKey();
if (validator && !squelch_.expireSquelch(*validator))
{
overlay_.reportOutboundTraffic(
TrafficCount::Category::SquelchSuppressed,
static_cast<int>(m->getBuffer(compressionEnabled_).size()));
return;
}
// report categorized outgoing traffic
self->overlay_.reportOutboundTraffic(
safeCast<TrafficCount::Category>(m->getCategory()),
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
// report categorized outgoing traffic
overlay_.reportOutboundTraffic(
safeCast<TrafficCount::Category>(m->getCategory()),
static_cast<int>(m->getBuffer(compressionEnabled_).size()));
// report total outgoing traffic
self->overlay_.reportOutboundTraffic(
TrafficCount::Category::Total,
static_cast<int>(m->getBuffer(self->compressionEnabled_).size()));
// report total outgoing traffic
overlay_.reportOutboundTraffic(
TrafficCount::Category::Total, static_cast<int>(m->getBuffer(compressionEnabled_).size()));
auto sendqSize = self->sendQueue_.size();
auto sendqSize = sendQueue_.size();
if (sendqSize < Tuning::kTargetSendQueue)
{
// To detect a peer that does not read from their
// side of the connection, we expect a peer to have
// a small sendq periodically
self->largeSendq_ = 0;
}
else if (
auto sink = self->journal_.debug();
sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0)
{
std::string const n = self->name();
sink << n << " sendq: " << sendqSize;
}
if (sendqSize < Tuning::kTargetSendQueue)
{
// To detect a peer that does not read from their
// side of the connection, we expect a peer to have
// a small senq periodically
largeSendq_ = 0;
}
else if (auto sink = journal_.debug(); sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0)
{
std::string const n = name();
sink << n << " sendq: " << sendqSize;
}
self->sendQueue_.push(m);
sendQueue_.push(m);
if (sendqSize != 0)
return;
if (sendqSize != 0)
return;
boost::asio::async_write(
self->stream_,
boost::asio::buffer(self->sendQueue_.front()->getBuffer(self->compressionEnabled_)),
bind_executor(
self->strand_,
std::bind(
&PeerImp::onWriteMessage, self, std::placeholders::_1, std::placeholders::_2)));
});
boost::asio::async_write(
stream_,
boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)),
bind_executor(
strand_,
std::bind(
&PeerImp::onWriteMessage,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2)));
}
void
PeerImp::sendTxQueue()
{
dispatch(strand_, [self = shared_from_this()]() {
if (!self->txQueue_.empty())
{
protocol::TMHaveTransactions ht;
std::ranges::for_each(
self->txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); });
JLOG(self->pJournal_.trace()) << "sendTxQueue " << self->txQueue_.size();
self->txQueue_.clear();
self->send(std::make_shared<Message>(ht, protocol::mtHAVE_TRANSACTIONS));
}
});
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::sendTxQueue, shared_from_this()));
return;
}
if (!txQueue_.empty())
{
protocol::TMHaveTransactions ht;
std::ranges::for_each(
txQueue_, [&](auto const& hash) { ht.add_hashes(hash.data(), hash.size()); });
JLOG(pJournal_.trace()) << "sendTxQueue " << txQueue_.size();
txQueue_.clear();
send(std::make_shared<Message>(ht, protocol::mtHAVE_TRANSACTIONS));
}
}
void
PeerImp::addTxQueue(uint256 const& hash)
{
dispatch(strand_, [self = shared_from_this(), hash]() {
if (self->txQueue_.size() == reduce_relay::kMaxTxQueueSize)
{
JLOG(self->pJournal_.warn()) << "addTxQueue exceeds the cap";
self->sendTxQueue();
}
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::addTxQueue, shared_from_this(), hash));
return;
}
self->txQueue_.insert(hash);
JLOG(self->pJournal_.trace()) << "addTxQueue " << self->txQueue_.size();
});
if (txQueue_.size() == reduce_relay::kMaxTxQueueSize)
{
JLOG(pJournal_.warn()) << "addTxQueue exceeds the cap";
sendTxQueue();
}
txQueue_.insert(hash);
JLOG(pJournal_.trace()) << "addTxQueue " << txQueue_.size();
}
void
PeerImp::removeTxQueue(uint256 const& hash)
{
dispatch(strand_, [self = shared_from_this(), hash]() {
auto removed = self->txQueue_.erase(hash);
JLOG(self->pJournal_.trace()) << "removeTxQueue " << removed;
});
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind(&PeerImp::removeTxQueue, shared_from_this(), hash));
return;
}
auto removed = txQueue_.erase(hash);
JLOG(pJournal_.trace()) << "removeTxQueue " << removed;
}
void
PeerImp::charge(Resource::Charge const& fee, std::string const& context)
{
dispatch(strand_, [self = shared_from_this(), fee, context]() {
if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) &&
self->usage_.disconnect(self->pJournal_))
dispatch(strand_, [this, self = shared_from_this(), fee, context]() {
if ((usage_.charge(fee, context) == Resource::Disposition::Drop) &&
usage_.disconnect(pJournal_))
{
// Idempotent: only the first worker to observe Drop counts the
// metric and posts fail(). Without the guard, several queued
@@ -381,11 +405,11 @@ PeerImp::charge(Resource::Charge const& fee, std::string const& context)
// shutdowns. fail(std::string const&) self-posts to strand_
// when invoked off-strand.
bool expected = false;
if (self->chargeDisconnectFired_.compare_exchange_strong(
if (chargeDisconnectFired_.compare_exchange_strong(
expected, true, std::memory_order_acq_rel))
{
self->overlay_.incPeerDisconnectCharges();
self->fail("charge: Resources");
overlay_.incPeerDisconnectCharges();
fail("charge: Resources");
}
}
});
@@ -616,14 +640,20 @@ PeerImp::close()
void
PeerImp::fail(std::string const& reason)
{
dispatch(strand_, [self = shared_from_this(), reason]() {
if (self->journal_.active(beast::Severity::Warning) && self->socket_.is_open())
{
std::string const n = self->name();
JLOG(self->journal_.warn()) << n << " failed: " << reason;
}
self->close();
});
if (!strand_.running_in_this_thread())
{
post(
strand_,
std::bind(
(void (Peer::*)(std::string const&))&PeerImp::fail, shared_from_this(), reason));
return;
}
if (journal_.active(beast::Severity::Warning) && socket_.is_open())
{
std::string const n = name();
JLOG(journal_.warn()) << n << " failed: " << reason;
}
close();
}
void
@@ -2722,42 +2752,45 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMTransactions> const& m)
void
PeerImp::onMessage(std::shared_ptr<protocol::TMSquelch> const& m)
{
dispatch(strand_, [self = shared_from_this(), m]() {
if (!m->has_validatorpubkey())
{
self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey");
return;
}
auto validator = m->validatorpubkey();
auto const slice{makeSlice(validator)};
if (!publicKeyType(slice))
{
self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey");
return;
}
PublicKey const key(slice);
using on_message_fn = void (PeerImp::*)(std::shared_ptr<protocol::TMSquelch> const&);
if (!strand_.running_in_this_thread())
{
post(strand_, std::bind((on_message_fn)&PeerImp::onMessage, shared_from_this(), m));
return;
}
// Ignore the squelch for validator's own messages.
if (key == self->app_.getValidationPublicKey())
{
JLOG(self->pJournal_.debug())
<< "onMessage: TMSquelch discarding validator's squelch " << slice;
return;
}
if (!m->has_validatorpubkey())
{
fee_.update(Resource::kFeeInvalidData, "squelch no pubkey");
return;
}
auto validator = m->validatorpubkey();
auto const slice{makeSlice(validator)};
if (!publicKeyType(slice))
{
fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey");
return;
}
PublicKey const key(slice);
std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0;
if (!m->squelch())
{
self->squelch_.removeSquelch(key);
}
else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration}))
{
self->fee_.update(Resource::kFeeInvalidData, "squelch duration");
}
// Ignore the squelch for validator's own messages.
if (key == app_.getValidationPublicKey())
{
JLOG(pJournal_.debug()) << "onMessage: TMSquelch discarding validator's squelch " << slice;
return;
}
JLOG(self->pJournal_.debug())
<< "onMessage: TMSquelch " << slice << " " << self->id() << " " << duration;
});
std::uint32_t const duration = m->has_squelchduration() ? m->squelchduration() : 0;
if (!m->squelch())
{
squelch_.removeSquelch(key);
}
else if (!squelch_.addSquelch(key, std::chrono::seconds{duration}))
{
fee_.update(Resource::kFeeInvalidData, "squelch duration");
}
JLOG(pJournal_.debug()) << "onMessage: TMSquelch " << slice << " " << id() << " " << duration;
}
//--------------------------------------------------------------------------