Files
rippled/src/test/jtx/impl/Env.cpp
2026-02-19 23:30:00 +00:00

660 lines
19 KiB
C++

#include <test/jtx/Env.h>
#include <test/jtx/JSONRPCClient.h>
#include <test/jtx/balance.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/trust.h>
#include <test/jtx/utility.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/rpc/RPCCall.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/scope.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/json/to_string.h>
#include <xrpl/net/HTTPClient.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>
#include <memory>
#include <source_location>
namespace xrpl {
namespace test {
namespace jtx {
//------------------------------------------------------------------------------
Env::AppBundle::AppBundle(
beast::unit_test::suite& suite,
std::unique_ptr<Config> config,
std::unique_ptr<Logs> logs,
beast::severities::Severity thresh)
: AppBundle()
{
using namespace beast::severities;
if (logs)
{
setDebugLogSink(logs->makeSink("Debug", kFatal));
}
else
{
logs = std::make_unique<SuiteLogs>(suite);
// Use kFatal threshold to reduce noise from STObject.
setDebugLogSink(std::make_unique<SuiteJournalSink>("Debug", kFatal, suite));
}
auto timeKeeper_ = std::make_unique<ManualTimeKeeper>();
timeKeeper = timeKeeper_.get();
// Hack so we don't have to call Config::setup
HTTPClient::initializeSSLContext(
config->SSL_VERIFY_DIR, config->SSL_VERIFY_FILE, config->SSL_VERIFY, debugLog());
owned = make_Application(std::move(config), std::move(logs), std::move(timeKeeper_));
app = owned.get();
app->logs().threshold(thresh);
if (!app->setup({}))
Throw<std::runtime_error>("Env::AppBundle: setup failed");
timeKeeper->set(app->getLedgerMaster().getClosedLedger()->header().closeTime);
app->start(false /*don't start timers*/);
thread = std::thread([&]() { app->run(); });
client = makeJSONRPCClient(app->config());
}
Env::AppBundle::~AppBundle()
{
client.reset();
// Make sure all jobs finish, otherwise tests
// might not get the coverage they expect.
if (app)
{
app->getJobQueue().rendezvous();
app->signalStop("~AppBundle");
}
if (thread.joinable())
thread.join();
// Remove the debugLogSink before the suite goes out of scope.
setDebugLogSink(nullptr);
}
//------------------------------------------------------------------------------
std::shared_ptr<ReadView const>
Env::closed()
{
return app().getLedgerMaster().getClosedLedger();
}
bool
Env::close(NetClock::time_point closeTime, std::optional<std::chrono::milliseconds> consensusDelay)
{
// Round up to next distinguishable value
using namespace std::chrono_literals;
bool res = true;
closeTime += closed()->header().closeTimeResolution - 1s;
timeKeeper().set(closeTime);
// Go through the rpc interface unless we need to simulate
// a specific consensus delay.
if (consensusDelay)
app().getOPs().acceptLedger(consensusDelay);
else
{
auto resp = rpc("ledger_accept");
if (resp["result"]["status"] != std::string("success"))
{
std::string reason = "internal error";
if (resp.isMember("error_what"))
reason = resp["error_what"].asString();
else if (resp.isMember("error_message"))
reason = resp["error_message"].asString();
else if (resp.isMember("error"))
reason = resp["error"].asString();
JLOG(journal.error()) << "Env::close() failed: " << reason;
res = false;
}
}
timeKeeper().set(closed()->header().closeTime);
return res;
}
void
Env::memoize(Account const& account)
{
map_.emplace(account.id(), account);
}
Account const&
Env::lookup(AccountID const& id) const
{
auto const iter = map_.find(id);
if (iter == map_.end())
{
std::cout << "Unknown account: " << id << "\n";
Throw<std::runtime_error>("Env::lookup:: unknown account ID");
}
return iter->second;
}
Account const&
Env::lookup(std::string const& base58ID) const
{
auto const account = parseBase58<AccountID>(base58ID);
if (!account)
Throw<std::runtime_error>("Env::lookup: invalid account ID");
return lookup(*account);
}
PrettyAmount
Env::balance(Account const& account) const
{
auto const sle = le(account);
if (!sle)
return XRP(0);
return {sle->getFieldAmount(sfBalance), ""};
}
PrettyAmount
Env::balance(Account const& account, Issue const& issue) const
{
if (isXRP(issue.currency))
return balance(account);
auto const sle = le(keylet::line(account.id(), issue));
if (!sle)
return {STAmount(issue, 0), account.name()};
auto amount = sle->getFieldAmount(sfBalance);
amount.setIssuer(issue.account);
if (account.id() > issue.account)
amount.negate();
return {amount, lookup(issue.account).name()};
}
PrettyAmount
Env::balance(Account const& account, MPTIssue const& mptIssue) const
{
MPTID const id = mptIssue.getMptID();
if (!id)
return {STAmount(mptIssue, 0), account.name()};
AccountID const issuer = mptIssue.getIssuer();
if (account.id() == issuer)
{
// Issuer balance
auto const sle = le(keylet::mptIssuance(id));
if (!sle)
return {STAmount(mptIssue, 0), account.name()};
// Make it negative
STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true};
return {amount, lookup(issuer).name()};
}
else
{
// Holder balance
auto const sle = le(keylet::mptoken(id, account));
if (!sle)
return {STAmount(mptIssue, 0), account.name()};
STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)};
return {amount, lookup(issuer).name()};
}
}
PrettyAmount
Env::balance(Account const& account, Asset const& asset) const
{
return std::visit([&](auto const& issue) { return balance(account, issue); }, asset.value());
}
PrettyAmount
Env::limit(Account const& account, Issue const& issue) const
{
auto const sle = le(keylet::line(account.id(), issue));
if (!sle)
return {STAmount(issue, 0), account.name()};
auto const aHigh = account.id() > issue.account;
if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit))
return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()};
return {STAmount(issue, 0), account.name()};
}
std::uint32_t
Env::ownerCount(Account const& account) const
{
auto const sle = le(account);
if (!sle)
Throw<std::runtime_error>("missing account root");
return sle->getFieldU32(sfOwnerCount);
}
std::uint32_t
Env::seq(Account const& account) const
{
auto const sle = le(account);
if (!sle)
Throw<std::runtime_error>("missing account root");
return sle->getFieldU32(sfSequence);
}
std::shared_ptr<SLE const>
Env::le(Account const& account) const
{
return le(keylet::account(account.id()));
}
std::shared_ptr<SLE const>
Env::le(Keylet const& k) const
{
return current()->read(k);
}
void
Env::fund(bool setDefaultRipple, STAmount const& amount, Account const& account)
{
memoize(account);
if (setDefaultRipple)
{
// VFALCO NOTE Is the fee formula correct?
apply(
pay(master, account, amount + drops(current()->fees().base)),
jtx::seq(jtx::autofill),
fee(jtx::autofill),
sig(jtx::autofill));
apply(
fset(account, asfDefaultRipple),
jtx::seq(jtx::autofill),
fee(jtx::autofill),
sig(jtx::autofill));
require(flags(account, asfDefaultRipple));
}
else
{
apply(
pay(master, account, amount),
jtx::seq(jtx::autofill),
fee(jtx::autofill),
sig(jtx::autofill));
require(nflags(account, asfDefaultRipple));
}
require(jtx::balance(account, amount));
}
void
Env::trust(STAmount const& amount, Account const& account)
{
auto const start = balance(account);
apply(
jtx::trust(account, amount),
jtx::seq(jtx::autofill),
fee(jtx::autofill),
sig(jtx::autofill));
apply(
pay(master, account, drops(current()->fees().base)),
jtx::seq(jtx::autofill),
fee(jtx::autofill),
sig(jtx::autofill));
test.expect(balance(account) == start);
}
Env::ParsedResult
Env::parseResult(Json::Value const& jr)
{
auto error = [](ParsedResult& parsed, Json::Value const& object) {
// Use an error code that is not used anywhere in the transaction
// engine to distinguish this case.
parsed.ter = telENV_RPC_FAILED;
// Extract information about the error
if (!object.isObject())
return;
if (object.isMember(jss::error_code))
parsed.rpcCode = safe_cast<error_code_i>(object[jss::error_code].asInt());
if (object.isMember(jss::error_message))
parsed.rpcMessage = object[jss::error_message].asString();
if (object.isMember(jss::error))
parsed.rpcError = object[jss::error].asString();
if (object.isMember(jss::error_exception))
parsed.rpcException = object[jss::error_exception].asString();
};
ParsedResult parsed;
if (jr.isObject() && jr.isMember(jss::result))
{
auto const& result = jr[jss::result];
if (result.isMember(jss::engine_result_code))
{
parsed.ter = TER::fromInt(result[jss::engine_result_code].asInt());
parsed.rpcCode.emplace(rpcSUCCESS);
}
else
error(parsed, result);
}
else
error(parsed, jr);
return parsed;
}
void
Env::submit(JTx const& jt, std::source_location const& loc)
{
ParsedResult parsedResult;
auto const jr = [&]() {
if (jt.stx)
{
txid_ = jt.stx->getTransactionID();
Serializer s;
jt.stx->add(s);
auto const jr = rpc("submit", strHex(s.slice()));
parsedResult = parseResult(jr);
test.expect(parsedResult.ter, "ter uninitialized!");
ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);
return jr;
}
else
{
// Parsing failed or the JTx is
// otherwise missing the stx field.
parsedResult.ter = ter_ = temMALFORMED;
return Json::Value();
}
}();
return postconditions(jt, parsedResult, jr, loc);
}
void
Env::sign_and_submit(JTx const& jt, Json::Value params, std::source_location const& loc)
{
auto const account = lookup(jt.jv[jss::Account].asString());
auto const& passphrase = account.name();
Json::Value jr;
if (params.isNull())
{
// Use the command line interface
auto const jv = to_string(jt.jv);
jr = rpc("submit", passphrase, jv);
}
else
{
// Use the provided parameters, and go straight
// to the (RPC) client.
assert(params.isObject());
if (!params.isMember(jss::secret) && !params.isMember(jss::key_type) &&
!params.isMember(jss::seed) && !params.isMember(jss::seed_hex) &&
!params.isMember(jss::passphrase))
{
params[jss::secret] = passphrase;
}
params[jss::tx_json] = jt.jv;
jr = client().invoke("submit", params);
}
if (!txid_.parseHex(jr[jss::result][jss::tx_json][jss::hash].asString()))
txid_.zero();
ParsedResult const parsedResult = parseResult(jr);
test.expect(parsedResult.ter, "ter uninitialized!");
ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);
return postconditions(jt, parsedResult, jr, loc);
}
void
Env::postconditions(
JTx const& jt,
ParsedResult const& parsed,
Json::Value const& jr,
std::source_location const& loc)
{
auto const line = jt.testLine ? " (" + to_string(*jt.testLine) + ")" : "";
auto const locStr = std::string("(") + loc.file_name() + ":" + to_string(loc.line()) + ")";
bool bad = !test.expect(parsed.ter, "apply " + locStr + ": No ter result!" + line);
bad =
(jt.ter && parsed.ter &&
!test.expect(
*parsed.ter == *jt.ter,
"apply " + locStr + ": Got " + transToken(*parsed.ter) + " (" +
transHuman(*parsed.ter) + "); Expected " + transToken(*jt.ter) + " (" +
transHuman(*jt.ter) + ")" + line));
using namespace std::string_literals;
bad = (jt.rpcCode &&
!test.expect(
parsed.rpcCode == jt.rpcCode->first && parsed.rpcMessage == jt.rpcCode->second,
"apply " + locStr + ": Got RPC result "s +
(parsed.rpcCode ? RPC::get_error_info(*parsed.rpcCode).token.c_str()
: "NO RESULT") +
" (" + parsed.rpcMessage + "); Expected " +
RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" +
jt.rpcCode->second + ")" + line)) ||
bad;
// If we have an rpcCode (just checked), then the rpcException check is
// optional - the 'error' field may not be defined, but if it is, it must
// match rpcError.
bad = (jt.rpcException &&
!test.expect(
(jt.rpcCode && parsed.rpcError.empty()) ||
(parsed.rpcError == jt.rpcException->first &&
(!jt.rpcException->second || parsed.rpcException == *jt.rpcException->second)),
"apply " + locStr + ": Got RPC result "s + parsed.rpcError + " (" +
parsed.rpcException + "); Expected " + jt.rpcException->first + " (" +
jt.rpcException->second.value_or("n/a") + ")" + line)) ||
bad;
if (bad)
{
test.log << pretty(jt.jv) << std::endl;
if (jr)
test.log << pretty(jr) << std::endl;
// Don't check postconditions if
// we didn't get the expected result.
return;
}
if (trace_)
{
if (trace_ > 0)
--trace_;
test.log << pretty(jt.jv) << std::endl;
}
for (auto const& f : jt.require)
f(*this);
}
std::shared_ptr<STObject const>
Env::meta()
{
if (current()->txCount() != 0)
{
// close the ledger if it has not already been closed
// (metadata is not finalized until the ledger is closed)
close();
}
auto const item = closed()->txRead(txid_);
auto const result = item.second;
if (result == nullptr)
{
test.log << "Env::meta: no metadata for txid: " << txid_ << std::endl;
test.log << "This is probably because the transaction failed with a "
"non-tec error."
<< std::endl;
Throw<std::runtime_error>("Env::meta: no metadata for txid");
}
return result;
}
std::shared_ptr<STTx const>
Env::tx() const
{
return current()->txRead(txid_).first;
}
void
Env::autofill_sig(JTx& jt)
{
auto& jv = jt.jv;
scope_success success([&]() {
// Call all the post-signers after the main signers or autofill are done
for (auto const& signer : jt.postSigners)
signer(*this, jt);
});
// Call all the main signers
if (!jt.mainSigners.empty())
{
for (auto const& signer : jt.mainSigners)
signer(*this, jt);
return;
}
// If the sig is still needed, get it here.
if (!jt.fill_sig)
return;
auto const account = jv.isMember(sfDelegate.jsonName)
? lookup(jv[sfDelegate.jsonName].asString())
: lookup(jv[jss::Account].asString());
if (!app().checkSigs())
{
jv[jss::SigningPubKey] = strHex(account.pk().slice());
// dummy sig otherwise STTx is invalid
jv[jss::TxnSignature] = "00";
return;
}
auto const ar = le(account);
if (ar && ar->isFieldPresent(sfRegularKey))
jtx::sign(jv, lookup(ar->getAccountID(sfRegularKey)));
else
jtx::sign(jv, account);
}
void
Env::autofill(JTx& jt)
{
auto& jv = jt.jv;
if (jt.fill_fee)
jtx::fill_fee(jv, *current());
if (jt.fill_seq)
jtx::fill_seq(jv, *current());
if (jt.fill_netid)
{
uint32_t networkID = app().getNetworkIDService().getNetworkID();
if (!jv.isMember(jss::NetworkID) && networkID > 1024)
jv[jss::NetworkID] = std::to_string(networkID);
}
// Must come last
try
{
autofill_sig(jt);
}
catch (parse_error const&)
{
if (!parseFailureExpected_)
test.log << "parse failed:\n" << pretty(jv) << std::endl;
Rethrow();
}
}
std::shared_ptr<STTx const>
Env::st(JTx const& jt)
{
// The parse must succeed, since we
// generated the JSON ourselves.
std::optional<STObject> obj;
try
{
obj = jtx::parse(jt.jv);
}
catch (jtx::parse_error const&)
{
test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl;
Rethrow();
}
try
{
return sterilize(STTx{std::move(*obj)});
}
catch (std::exception const&)
{
}
return nullptr;
}
std::shared_ptr<STTx const>
Env::ust(JTx const& jt)
{
// The parse must succeed, since we
// generated the JSON ourselves.
std::optional<STObject> obj;
try
{
obj = jtx::parse(jt.jv);
}
catch (jtx::parse_error const&)
{
test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl;
Rethrow();
}
try
{
return std::make_shared<STTx const>(std::move(*obj));
}
catch (std::exception const&)
{
}
return nullptr;
}
Json::Value
Env::do_rpc(
unsigned apiVersion,
std::vector<std::string> const& args,
std::unordered_map<std::string, std::string> const& headers)
{
auto response = rpcClient(args, app().config(), app().logs(), apiVersion, headers);
for (unsigned ctr = 0; (ctr < retries_) and (response.first == rpcINTERNAL); ++ctr)
{
JLOG(journal.error()) << "Env::do_rpc error, retrying, attempt #" << ctr + 1 << " ...";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
response = rpcClient(args, app().config(), app().logs(), apiVersion, headers);
}
return response.second;
}
void
Env::enableFeature(uint256 const feature)
{
// Env::close() must be called for feature
// enable to take place.
app().config().features.insert(feature);
}
void
Env::disableFeature(uint256 const feature)
{
// Env::close() must be called for feature
// enable to take place.
app().config().features.erase(feature);
}
} // namespace jtx
} // namespace test
} // namespace xrpl