mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-23 13:40:15 +00:00
Compare commits
2 Commits
copilot/ad
...
pratik/net
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
926e07b1ed | ||
|
|
1dc285ddd4 |
@@ -42,7 +42,6 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta
|
||||
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
|
||||
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
|
||||
- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)).
|
||||
- `submit`: Augmented response fields (`accepted`, `applied`, `broadcast`, `queued`, `kept`, `account_sequence_next`, `account_sequence_available`, `open_ledger_cost`, `validated_ledger_index`) are now included in sign-and-submit mode. Previously, these fields were only returned when submitting a binary transaction blob. ([#6304](https://github.com/XRPLF/rippled/pull/6304))
|
||||
|
||||
## XRP Ledger server version 3.3.0
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ inline constexpr auto kPropagateExceptions = [](std::exception_ptr ePtr) {
|
||||
*
|
||||
* @tparam Ctx The type of the context/strand
|
||||
* @tparam F The type of the function to execute
|
||||
* @param ctx The execution context
|
||||
* @param ctx An execution context (e.g. `io_context`), an executor, or a
|
||||
* strand. A strand is used as-is; anything else is wrapped in a new strand.
|
||||
* @param func The function to execute. Must return `void`
|
||||
*/
|
||||
template <typename Ctx, typename F>
|
||||
@@ -74,7 +75,7 @@ spawn(Ctx&& ctx, F&& func)
|
||||
else
|
||||
{
|
||||
boost::asio::spawn(
|
||||
boost::asio::make_strand(boost::asio::get_associated_executor(std::forward<Ctx>(ctx))),
|
||||
boost::asio::make_strand(std::forward<Ctx>(ctx)),
|
||||
std::forward<F>(func),
|
||||
impl::kPropagateExceptions);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
@@ -155,6 +154,11 @@ public:
|
||||
boost::asio::ip::resolver_query_base::numeric_service);
|
||||
query_ = query;
|
||||
|
||||
// Each site starts with a clean error state. shutdown_ records the first
|
||||
// failure of the current attempt only, so a fallback to the next entry
|
||||
// in deqSites_ is resolved and connected like a fresh request.
|
||||
shutdown_.clear();
|
||||
|
||||
try
|
||||
{
|
||||
deadline_.expires_after(timeout_);
|
||||
@@ -164,13 +168,16 @@ public:
|
||||
shutdown_ = e.code();
|
||||
|
||||
JLOG(j_.trace()) << "expires_after: " << shutdown_.message();
|
||||
deadline_.async_wait([self = shared_from_this()](boost::system::error_code const& ec) {
|
||||
self->handleDeadline(ec);
|
||||
});
|
||||
}
|
||||
|
||||
if (!shutdown_)
|
||||
{
|
||||
// A set expiry fires only if a waiter is registered, so the wait
|
||||
// is armed whenever expires_after succeeded.
|
||||
deadline_.async_wait([self = shared_from_this()](boost::system::error_code const& ec) {
|
||||
self->handleDeadline(ec);
|
||||
});
|
||||
|
||||
JLOG(j_.trace()) << "Resolving: " << deqSites_[0];
|
||||
|
||||
resolver_.async_resolve(
|
||||
@@ -183,9 +190,10 @@ public:
|
||||
self->handleResolve(ecResult, results);
|
||||
});
|
||||
}
|
||||
|
||||
if (shutdown_)
|
||||
else
|
||||
{
|
||||
invokeComplete(shutdown_);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -195,41 +203,42 @@ public:
|
||||
{
|
||||
// Timer canceled because deadline no longer needed.
|
||||
JLOG(j_.trace()) << "Deadline cancelled.";
|
||||
|
||||
// Aborter is done.
|
||||
return;
|
||||
}
|
||||
else if (ecResult)
|
||||
{
|
||||
JLOG(j_.trace()) << "Deadline error: " << deqSites_[0] << ": " << ecResult.message();
|
||||
|
||||
// Can't do anything sound.
|
||||
std::abort();
|
||||
// A handler that was already queued when httpsNext() re-armed the
|
||||
// timer for the next site sees an expiry in the future. It belongs to
|
||||
// the finished attempt and must leave the new one alone. Strictly
|
||||
// greater: a genuine expiry observed within the same clock tick has
|
||||
// expiry == now and must still be acted on.
|
||||
if (deadline_.expiry() > std::chrono::steady_clock::now())
|
||||
{
|
||||
JLOG(j_.trace()) << "Stale deadline ignored.";
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
JLOG(j_.trace()) << "Deadline: " << (ecResult ? ecResult.message() : "arrived");
|
||||
|
||||
// Mark us as shutting down. A wait error ends the attempt exactly as a
|
||||
// timeout does.
|
||||
if (!shutdown_)
|
||||
{
|
||||
JLOG(j_.trace()) << "Deadline arrived.";
|
||||
|
||||
// Mark us as shutting down.
|
||||
// XXX Use our own error code.
|
||||
shutdown_ = boost::system::error_code{
|
||||
boost::system::errc::bad_address, boost::system::system_category()};
|
||||
|
||||
// Cancel any resolving.
|
||||
resolver_.cancel();
|
||||
|
||||
// Stop the transaction.
|
||||
socket_.asyncShutdown([self = shared_from_this()](boost::system::error_code const& ec) {
|
||||
self->handleShutdown(ec);
|
||||
});
|
||||
shutdown_ =
|
||||
ecResult ? ecResult : boost::system::error_code{boost::asio::error::timed_out};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
handleShutdown(boost::system::error_code const& ecResult)
|
||||
{
|
||||
if (ecResult)
|
||||
// Cancel any resolving.
|
||||
resolver_.cancel();
|
||||
|
||||
// Close the transport rather than negotiating a TLS shutdown: the
|
||||
// negotiation waits on the peer, and the deadline has already
|
||||
// passed. The pending operation's handler then runs with
|
||||
// shutdown_ set and reports the timeout.
|
||||
boost::system::error_code ec;
|
||||
socket_.lowestLayer().close(ec);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(j_.trace()) << "Shutdown error: " << deqSites_[0] << ": " << ecResult.message();
|
||||
JLOG(j_.trace()) << "Deadline close error: " << ec.message();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +375,18 @@ public:
|
||||
void
|
||||
handleHeader(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
|
||||
{
|
||||
// A read error, or a deadline that closed the transport while this read
|
||||
// was pending, ends the attempt here rather than parsing an incomplete
|
||||
// header. The deadline records its own code first, so it wins.
|
||||
if (!shutdown_)
|
||||
shutdown_ = ecResult;
|
||||
|
||||
if (shutdown_)
|
||||
{
|
||||
invokeComplete(shutdown_);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string strHeader{
|
||||
{std::istreambuf_iterator<char>(&header_)}, std::istreambuf_iterator<char>()};
|
||||
JLOG(j_.trace()) << "Header: \"" << strHeader << "\"";
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
@@ -207,13 +206,6 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const objectID = ctx.tx[~sfObjectID];
|
||||
ctx.rules.enabled(fixCleanup3_5_0) && objectID && *objectID == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "preflight: sfObjectID must not be zero";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -1163,25 +1163,6 @@ public:
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_5_0, a zero ObjectID is malformed.
|
||||
// Pre-fixCleanup3_5_0 path is unreachable so it is not testable.
|
||||
if (features[fixCleanup3_5_0])
|
||||
{
|
||||
uint256 const zeroObjectID{};
|
||||
|
||||
env(sponsor::transfer(alice, tfSponsorshipEnd, zeroObjectID), Ter(temMALFORMED));
|
||||
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, zeroObjectID),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(temMALFORMED));
|
||||
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, zeroObjectID),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(temMALFORMED));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -2,104 +2,20 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/JTx.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/pay.h>
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class Submit_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testAugmentedFields()
|
||||
{
|
||||
testcase("Augmented fields in sign-and-submit mode");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// Enable signing support in config
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
static std::string const kSigningSupportCfg =
|
||||
std::string("[") + Sections::kSigningSupport + "]\ntrue";
|
||||
cfg->loadFromString(kSigningSupportCfg);
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Test 1: Sign-and-submit mode should return augmented fields
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::tx_json][jss::TransactionType] = jss::Payment;
|
||||
jv[jss::tx_json][jss::Account] = alice.human();
|
||||
jv[jss::tx_json][jss::Destination] = bob.human();
|
||||
jv[jss::tx_json][jss::Amount] = XRP(100).value().getJson();
|
||||
jv[jss::secret] = alice.name();
|
||||
|
||||
auto const result = env.rpc("json", "submit", to_string(jv))[jss::result];
|
||||
|
||||
// These are the augmented fields that should be present
|
||||
BEAST_EXPECT(result.isMember(jss::engine_result));
|
||||
BEAST_EXPECT(result.isMember(jss::engine_result_code));
|
||||
BEAST_EXPECT(result.isMember(jss::engine_result_message));
|
||||
|
||||
// New augmented fields from issue #3125
|
||||
BEAST_EXPECT(result.isMember(jss::accepted));
|
||||
BEAST_EXPECT(result.isMember(jss::applied));
|
||||
BEAST_EXPECT(result.isMember(jss::broadcast));
|
||||
BEAST_EXPECT(result.isMember(jss::queued));
|
||||
BEAST_EXPECT(result.isMember(jss::kept));
|
||||
|
||||
// Current ledger state fields
|
||||
BEAST_EXPECT(result.isMember(jss::account_sequence_next));
|
||||
BEAST_EXPECT(result.isMember(jss::account_sequence_available));
|
||||
BEAST_EXPECT(result.isMember(jss::open_ledger_cost));
|
||||
BEAST_EXPECT(result.isMember(jss::validated_ledger_index));
|
||||
|
||||
// Verify basic transaction fields
|
||||
BEAST_EXPECT(result.isMember(jss::tx_blob));
|
||||
BEAST_EXPECT(result.isMember(jss::tx_json));
|
||||
}
|
||||
|
||||
// Test 2: Binary blob mode should also return augmented fields (regression test)
|
||||
{
|
||||
auto jt = env.jt(pay(alice, bob, XRP(100)));
|
||||
Serializer s;
|
||||
jt.stx->add(s);
|
||||
|
||||
auto const result = env.rpc("submit", strHex(s.slice()))[jss::result];
|
||||
|
||||
// Verify augmented fields are present in binary mode too
|
||||
BEAST_EXPECT(result.isMember(jss::engine_result));
|
||||
BEAST_EXPECT(result.isMember(jss::accepted));
|
||||
BEAST_EXPECT(result.isMember(jss::applied));
|
||||
BEAST_EXPECT(result.isMember(jss::broadcast));
|
||||
BEAST_EXPECT(result.isMember(jss::queued));
|
||||
BEAST_EXPECT(result.isMember(jss::kept));
|
||||
BEAST_EXPECT(result.isMember(jss::account_sequence_next));
|
||||
BEAST_EXPECT(result.isMember(jss::account_sequence_available));
|
||||
BEAST_EXPECT(result.isMember(jss::open_ledger_cost));
|
||||
BEAST_EXPECT(result.isMember(jss::validated_ledger_index));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFailHardValidation()
|
||||
{
|
||||
@@ -173,7 +89,6 @@ public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAugmentedFields();
|
||||
testFailHardValidation();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/co_spawn.hpp> // IWYU pragma: keep
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/socket_base.hpp>
|
||||
@@ -19,8 +20,10 @@
|
||||
#include <helpers/TestSink.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -32,6 +35,15 @@ namespace {
|
||||
// Simple HTTP server using Beast for testing
|
||||
class TestHTTPServer
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* What the server does with a connection once it has read the request:
|
||||
* Reply serves the configured status, headers and body; Stall holds the
|
||||
* connection open and never replies; CloseAfterRead hangs up without
|
||||
* replying.
|
||||
*/
|
||||
enum class Behaviour { Reply, Stall, CloseAfterRead };
|
||||
|
||||
private:
|
||||
boost::asio::io_context ioc_;
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
@@ -45,13 +57,28 @@ private:
|
||||
std::string responseBody_;
|
||||
unsigned int statusCode_{200};
|
||||
|
||||
/**
|
||||
* How a connection is handled once its request is read. Anything other
|
||||
* than Reply leaves the client to finish through its own error handling.
|
||||
*/
|
||||
Behaviour behaviour_{Behaviour::Reply};
|
||||
|
||||
/**
|
||||
* The socket of the most recent stalled connection. Holding it open sends
|
||||
* no EOF to the client, so the client's read stays pending until the
|
||||
* client's own deadline fires and closes the client side.
|
||||
*/
|
||||
std::optional<boost::asio::ip::tcp::socket> stalledSocket_;
|
||||
|
||||
beast::Journal j_;
|
||||
|
||||
public:
|
||||
TestHTTPServer() : acceptor_(ioc_), j_(TestSink::instance())
|
||||
{
|
||||
// Bind to any available port
|
||||
endpoint_ = {boost::asio::ip::tcp::v4(), 0};
|
||||
// Bind to a fixed loopback address (rather than 0.0.0.0) so that a
|
||||
// sibling loopback address such as 127.0.0.2 has no listener, which
|
||||
// the fallback test relies on.
|
||||
endpoint_ = {boost::asio::ip::make_address("127.0.0.1"), 0};
|
||||
acceptor_.open(endpoint_.protocol());
|
||||
acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
|
||||
acceptor_.bind(endpoint_);
|
||||
@@ -103,6 +130,18 @@ public:
|
||||
statusCode_ = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose what happens to each connection after its request is read.
|
||||
*
|
||||
* @param behaviour Reply to serve the configured response, Stall to hold
|
||||
* the connection open with no reply, CloseAfterRead to hang up instead.
|
||||
*/
|
||||
void
|
||||
setBehaviour(Behaviour behaviour)
|
||||
{
|
||||
behaviour_ = behaviour;
|
||||
}
|
||||
|
||||
void
|
||||
stop()
|
||||
{
|
||||
@@ -155,6 +194,24 @@ private:
|
||||
co_await boost::beast::http::async_read(
|
||||
socket, buffer, req, boost::asio::use_awaitable);
|
||||
|
||||
if (behaviour_ == Behaviour::Stall)
|
||||
{
|
||||
// Hold the connection open and never reply. Moving the socket
|
||||
// into a member keeps it alive after this coroutine returns,
|
||||
// so the client's read stays pending until its own deadline
|
||||
// fires. The accept loop continues and stop() can still end it.
|
||||
stalledSocket_.emplace(std::move(socket));
|
||||
co_return;
|
||||
}
|
||||
|
||||
if (behaviour_ == Behaviour::CloseAfterRead)
|
||||
{
|
||||
// Hang up without replying, so the client's header read ends
|
||||
// with EOF and no bytes.
|
||||
socket.close();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Create response
|
||||
boost::beast::http::response<boost::beast::http::string_body> res;
|
||||
res.version(req.version());
|
||||
@@ -366,3 +423,143 @@ TEST_F(HTTPClientTest, different_status_codes)
|
||||
EXPECT_EQ(resultStatus, static_cast<int>(status));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(HTTPClientTest, request_times_out_on_stalled_peer)
|
||||
{
|
||||
// A peer that reads the request but never replies must not leave the
|
||||
// client waiting: the request deadline fires and completes the handler
|
||||
// exactly once with timed_out. This depends on the deadline wait being
|
||||
// armed on the normal, non-throwing expires_after path.
|
||||
TestHTTPServer server;
|
||||
server.setBehaviour(TestHTTPServer::Behaviour::Stall);
|
||||
|
||||
int completions{0};
|
||||
int resultStatus{-1};
|
||||
boost::system::error_code resultError;
|
||||
|
||||
HTTPClient::get(
|
||||
false, // no SSL
|
||||
server.ioc(),
|
||||
"127.0.0.1",
|
||||
server.port(),
|
||||
"/stall",
|
||||
1024, // max response size
|
||||
std::chrono::seconds(1),
|
||||
[&](boost::system::error_code const& ec, int status, std::string const&) -> bool {
|
||||
resultError = ec;
|
||||
resultStatus = status;
|
||||
++completions;
|
||||
// Close the acceptor so the accept loop ends and run_for drains.
|
||||
server.stop();
|
||||
return false; // don't retry
|
||||
},
|
||||
j_);
|
||||
|
||||
// Bounded wall-clock drive; the 1s deadline must fire well within this.
|
||||
server.ioc().run_for(std::chrono::seconds(4));
|
||||
// Stop unconditionally so the accept loop ends and the fixture's
|
||||
// finished() check holds even when the client never completes; a
|
||||
// regression then fails an EXPECT instead of aborting the binary.
|
||||
server.stop();
|
||||
server.ioc().poll();
|
||||
|
||||
EXPECT_EQ(completions, 1);
|
||||
EXPECT_EQ(resultError, boost::asio::error::timed_out);
|
||||
EXPECT_EQ(resultStatus, 0);
|
||||
EXPECT_TRUE(server.finished());
|
||||
}
|
||||
|
||||
TEST_F(HTTPClientTest, falls_back_to_next_site_after_connect_failure)
|
||||
{
|
||||
// When the first site cannot be reached, the client must fall back to the
|
||||
// next site and report that site's result, not the first site's connect
|
||||
// error. This depends on shutdown_ being cleared at the start of each
|
||||
// attempt in httpsNext().
|
||||
TestHTTPServer server;
|
||||
std::string const testBody = "fallback body";
|
||||
server.setResponseBody(testBody);
|
||||
server.setHeader("Content-Length", std::to_string(testBody.size()));
|
||||
|
||||
// First site: a loopback address the server does not listen on. Where the
|
||||
// whole 127/8 block is local (Linux) the connect is refused at once; where
|
||||
// 127.0.0.2 is not a configured loopback address (macOS) it is unreachable
|
||||
// and the 1 s deadline ends the attempt instead. Either way the client
|
||||
// must move on to the second site, which is the server.
|
||||
std::deque<std::string> const sites{"127.0.0.2", "127.0.0.1"};
|
||||
|
||||
int completions{0};
|
||||
int resultStatus{-1};
|
||||
std::string resultData;
|
||||
boost::system::error_code resultError;
|
||||
|
||||
HTTPClient::get(
|
||||
false, // no SSL
|
||||
server.ioc(),
|
||||
sites,
|
||||
server.port(),
|
||||
"/fallback",
|
||||
1024, // max response size
|
||||
std::chrono::seconds(1),
|
||||
[&](boost::system::error_code const& ec, int status, std::string const& data) -> bool {
|
||||
resultError = ec;
|
||||
resultStatus = status;
|
||||
resultData = data;
|
||||
++completions;
|
||||
server.stop();
|
||||
return false; // don't retry
|
||||
},
|
||||
j_);
|
||||
|
||||
// Bounded wall-clock drive: worst case is one 1 s deadline on the first
|
||||
// site followed by the real exchange on the second.
|
||||
server.ioc().run_for(std::chrono::seconds(6));
|
||||
server.stop();
|
||||
server.ioc().poll();
|
||||
|
||||
EXPECT_EQ(completions, 1);
|
||||
EXPECT_FALSE(resultError);
|
||||
EXPECT_EQ(resultStatus, 200);
|
||||
EXPECT_EQ(resultData, testBody);
|
||||
EXPECT_TRUE(server.finished());
|
||||
}
|
||||
|
||||
TEST_F(HTTPClientTest, reports_read_error_when_peer_closes_without_reply)
|
||||
{
|
||||
// A peer that reads the request and hangs up must surface the read error
|
||||
// itself, not a parse failure of the empty header buffer. This depends on
|
||||
// handleHeader() recording its own error code before it looks at the
|
||||
// buffer. The client timeout is longer than the drive window so that a
|
||||
// timeout cannot stand in for the read error.
|
||||
TestHTTPServer server;
|
||||
server.setBehaviour(TestHTTPServer::Behaviour::CloseAfterRead);
|
||||
|
||||
int completions{0};
|
||||
int resultStatus{-1};
|
||||
boost::system::error_code resultError;
|
||||
|
||||
HTTPClient::get(
|
||||
false, // no SSL
|
||||
server.ioc(),
|
||||
"127.0.0.1",
|
||||
server.port(),
|
||||
"/hangup",
|
||||
1024, // max response size
|
||||
std::chrono::seconds(10),
|
||||
[&](boost::system::error_code const& ec, int status, std::string const&) -> bool {
|
||||
resultError = ec;
|
||||
resultStatus = status;
|
||||
++completions;
|
||||
server.stop();
|
||||
return false; // don't retry
|
||||
},
|
||||
j_);
|
||||
|
||||
server.ioc().run_for(std::chrono::seconds(4));
|
||||
server.stop();
|
||||
server.ioc().poll();
|
||||
|
||||
EXPECT_EQ(completions, 1);
|
||||
EXPECT_EQ(resultError, boost::asio::error::eof);
|
||||
EXPECT_EQ(resultStatus, 0);
|
||||
EXPECT_TRUE(server.finished());
|
||||
}
|
||||
|
||||
75
src/tests/libxrpl/server/Spawn.cpp
Normal file
75
src/tests/libxrpl/server/Spawn.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include <xrpl/server/detail/Spawn.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
// util::spawn dispatches on whether its argument is already a strand. In-tree
|
||||
// callers pass a strand, so these tests are what exercise the non-strand branch
|
||||
// (which wraps the argument in a new strand): they force it to compile and run
|
||||
// for a bare io_context and for an io_context executor. A coroutine spawned on
|
||||
// each context form must execute exactly once while the io_context runs.
|
||||
|
||||
TEST(SpawnTest, runs_on_io_context_lvalue)
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
int runs = 0;
|
||||
util::spawn(ioc, [&](boost::asio::yield_context) { ++runs; });
|
||||
|
||||
EXPECT_EQ(runs, 0); // deferred until the context runs
|
||||
ioc.run();
|
||||
EXPECT_EQ(runs, 1);
|
||||
}
|
||||
|
||||
TEST(SpawnTest, runs_on_executor_lvalue)
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
auto executor = ioc.get_executor();
|
||||
int runs = 0;
|
||||
util::spawn(executor, [&](boost::asio::yield_context) { ++runs; });
|
||||
|
||||
EXPECT_EQ(runs, 0);
|
||||
ioc.run();
|
||||
EXPECT_EQ(runs, 1);
|
||||
}
|
||||
|
||||
TEST(SpawnTest, runs_on_strand)
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
auto strand = boost::asio::make_strand(ioc);
|
||||
int runs = 0;
|
||||
util::spawn(strand, [&](boost::asio::yield_context) { ++runs; });
|
||||
|
||||
EXPECT_EQ(runs, 0);
|
||||
ioc.run();
|
||||
EXPECT_EQ(runs, 1);
|
||||
}
|
||||
|
||||
TEST(SpawnTest, propagates_exception_to_run)
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
util::spawn(
|
||||
ioc, [](boost::asio::yield_context) { throw std::runtime_error("spawned failure"); });
|
||||
|
||||
// kPropagateExceptions must rethrow out of io_context::run(), preserving
|
||||
// both the type and the message rather than swallowing the exception.
|
||||
try
|
||||
{
|
||||
ioc.run();
|
||||
FAIL() << "expected the spawned exception to propagate";
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
EXPECT_STREQ(e.what(), "spawned failure");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -307,6 +307,9 @@ ConnectAttempt::onWrite(error_code ec)
|
||||
return;
|
||||
}
|
||||
|
||||
// The upgrade response is bounded by the same timer as every other step
|
||||
// of the attempt; onRead() cancels it on entry.
|
||||
setTimer();
|
||||
boost::beast::http::async_read(
|
||||
stream_,
|
||||
readBuf_,
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/NetworkIDService.h>
|
||||
@@ -810,8 +809,6 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion)
|
||||
jvResult[jss::engine_result] = sToken;
|
||||
jvResult[jss::engine_result_code] = tpTrans->getResult();
|
||||
jvResult[jss::engine_result_message] = sHuman;
|
||||
|
||||
rpc::populateAugmentedSubmitFields(jvResult, tpTrans);
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
@@ -825,33 +822,6 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
populateAugmentedSubmitFields(
|
||||
json::Value& jvResult,
|
||||
std::shared_ptr<Transaction> const& transaction)
|
||||
{
|
||||
auto const submitResult = transaction->getSubmitResult();
|
||||
|
||||
jvResult[jss::accepted] = submitResult.any();
|
||||
jvResult[jss::applied] = submitResult.applied;
|
||||
jvResult[jss::broadcast] = submitResult.broadcast;
|
||||
jvResult[jss::queued] = submitResult.queued;
|
||||
jvResult[jss::kept] = submitResult.kept;
|
||||
|
||||
if (auto currentLedgerState = transaction->getCurrentLedgerState())
|
||||
{
|
||||
jvResult[jss::account_sequence_next] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->accountSeqNext);
|
||||
jvResult[jss::account_sequence_available] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->accountSeqAvail);
|
||||
jvResult[jss::open_ledger_cost] = to_string(currentLedgerState->minFeeRequired);
|
||||
jvResult[jss::validated_ledger_index] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->validatedLedger);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
[[nodiscard]] static XRPAmount
|
||||
getTxFee(Application const& app, Config const& config, json::Value tx)
|
||||
{
|
||||
|
||||
@@ -22,21 +22,6 @@ class TxQ;
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* Populate augmented submit fields into a JSON result.
|
||||
* This helper populates the submit result flags (accepted, applied,
|
||||
* broadcast, queued, kept) and current ledger state fields
|
||||
* (account_sequence_next, account_sequence_available, open_ledger_cost,
|
||||
* validated_ledger_index) from a Transaction pointer.
|
||||
*
|
||||
* @param jvResult The JSON result to populate
|
||||
* @param transaction The transaction containing the submit result and state
|
||||
*/
|
||||
void
|
||||
populateAugmentedSubmitFields(
|
||||
json::Value& jvResult,
|
||||
std::shared_ptr<Transaction> const& transaction);
|
||||
|
||||
json::Value
|
||||
getCurrentNetworkFee(
|
||||
Role const role,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
@@ -153,7 +155,24 @@ doSubmit(rpc::JsonContext& context)
|
||||
jvResult[jss::engine_result_code] = transaction->getResult();
|
||||
jvResult[jss::engine_result_message] = sHuman;
|
||||
|
||||
rpc::populateAugmentedSubmitFields(jvResult, transaction);
|
||||
auto const submitResult = transaction->getSubmitResult();
|
||||
|
||||
jvResult[jss::accepted] = submitResult.any();
|
||||
jvResult[jss::applied] = submitResult.applied;
|
||||
jvResult[jss::broadcast] = submitResult.broadcast;
|
||||
jvResult[jss::queued] = submitResult.queued;
|
||||
jvResult[jss::kept] = submitResult.kept;
|
||||
|
||||
if (auto currentLedgerState = transaction->getCurrentLedgerState())
|
||||
{
|
||||
jvResult[jss::account_sequence_next] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->accountSeqNext);
|
||||
jvResult[jss::account_sequence_available] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->accountSeqAvail);
|
||||
jvResult[jss::open_ledger_cost] = to_string(currentLedgerState->minFeeRequired);
|
||||
jvResult[jss::validated_ledger_index] =
|
||||
safeCast<json::Value::UInt>(currentLedgerState->validatedLedger);
|
||||
}
|
||||
}
|
||||
|
||||
return jvResult;
|
||||
|
||||
Reference in New Issue
Block a user