Compare commits

..

2 Commits

Author SHA1 Message Date
Denis Angell
80b4c941f0 update headers 2024-07-04 10:29:43 +02:00
Denis Angell
7b5ed1c7c4 rough draft 2024-06-06 12:38:20 +02:00
22 changed files with 311 additions and 691 deletions

View File

@@ -588,6 +588,7 @@ target_sources (rippled PRIVATE
src/ripple/resource/impl/Consumer.cpp
src/ripple/resource/impl/Fees.cpp
src/ripple/resource/impl/ResourceManager.cpp
src/ripple/resource/impl/Tuning.cpp
#[===============================[
main sources:
subdir: rpc

View File

@@ -23,7 +23,9 @@
#
# 9. Misc Settings
#
# 10. Example Settings
# 10. Resource Settings
#
# 11. Example Settings
#
#-------------------------------------------------------------------------------
#
@@ -1565,7 +1567,41 @@
#
#-------------------------------------------------------------------------------
#
# 10. Example Settings
# 10. Resource Settings
#
#--------------------
# [resource]
#
# A set of key/value pair parameters to tune the performance of the
# transaction queue.
#
# warning_threshold = <number>
#
# Lorem Epsium....
#
# drop_threshold = <number>
#
# Lorem Epsium....
#
# decay_window_seconds = <number>
#
# Lorem Epsium....
#
# minimum_gossip_balance = <number>
#
# Lorem Epsium....
#
# seconds_until_expiration = <number>
#
# Lorem Epsium....
#
# gossip_expiration_seconds = <number>
#
# Lorem Epsium....
#
#
#
# 11. Example Settings
#
#--------------------
#

View File

@@ -352,6 +352,7 @@ public:
, validatorKeys_(*config_, m_journal)
, m_resourceManager(Resource::make_Manager(
config_->section("resource"),
m_collectorManager->collector(),
logs_->journal("Resource")))

View File

@@ -101,6 +101,7 @@ struct ConfigSection
#define SECTION_SWEEP_INTERVAL "sweep_interval"
#define SECTION_NETWORK_ID "network_id"
#define SECTION_IMPORT_VL_KEYS "import_vl_keys"
#define SECTION_RESOURCE "resource"
} // namespace ripple

View File

@@ -1,493 +0,0 @@
#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <sstream>
#include <stdexcept>
// make this typedef to keep dkim happy
typedef int _Bool;
#include <opendkim/dkim.h>
using namespace ripple;
namespace Email
{
enum EmailType : uint8_t
{
INVALID = 0,
REMIT = 1,
REKEY = 2
};
struct EmailDetails
{
std::string domain; // from address domain
std::string dkimDomain; // dkim signature domain
AccountID from;
std::string fromEmail;
std::optional<std::string> toEmail;
std::optional<AccountID> to;
EmailType emailType { EmailType::INVALID };
std::optional<STAmount> amount; // only valid if REMIT type
std::optional<AccountID> rekey; // only valid if REKEY type
};
class OpenDKIM
{
private:
DKIM_STAT status;
public:
DKIM_LIB* dkim_lib;
DKIM* dkim;
bool sane()
{
return !!dkim_lib && !!dkim;
}
OpenDKIM()
{
// do nothing
}
// setup is in its own function not the constructor to make failure graceful
bool setup(beast::Journal& j)
{
dkim_lib = dkim_init(nullptr, nullptr);
if (!dkim_lib)
{
JLOG(j.warn()) << "EmailAmendment: Failed to init dkim_lib.";
return false;
}
DKIM_STAT status;
DKIM* dkim = dkim_verify(dkim_lib, (uint8_t const*)"id", nullptr, &status);
if (!dkim_lib)
{
JLOG(j.warn()) << "EmailAmendment: Failed to init dkim_verify.";
return false;
}
return true;
}
~OpenDKIM()
{
if (dkim)
{
dkim_free(dkim);
dkim = nullptr;
}
if (dkim_lib)
{
dkim_close(dkim_lib);
dkim_lib = nullptr;
}
}
};
inline
std::optional<std::pair<std::string /* canonical email addr */, std::string /* canonical domain */>>
canonicalizeEmailAddress(const std::string& rawEmailAddr)
{
if (rawEmailAddr.empty())
return {};
// trim
auto start = std::find_if_not(str.begin(), str.end(), ::isspace);
auto end = std::find_if_not(str.rbegin(), str.rend(), ::isspace).base();
if (end >= start)
return {};
std::email = std::string(start, end);
if (email.empty())
return {};
// to lower
std::transform(email.begin(), email.end(), email.begin(), ::tolower);
// find the @
size_t atPos = email.find('@');
if (atPos == std::string::npos || atPos == email.size() - 1)
return {};
std::string localPart = email.substr(0, atPos);
std::string domain = email.substr(atPos + 1);
if (domain.empty() || localPart.empty())
return {};
// ensure there's only one @
if (domain.find('@') != std::string::npos)
return {};
// canonicalize domain part
{
std::string result = domain;
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
while (!result.empty() && result.back() == '.')
result.pop_back();
doamin = result;
}
if (domain.empty())
return {};
// canonicalize local part
{
std::string part = localPart;
part.erase(std::remove_if(
part.begin(), part.end(),
[](char c) { return c == '(' || c == ')' || std::isspace(c); }), part.end());
size_t plusPos = part.find('+');
if (plusPos != std::string::npos)
part = part.substr(0, plusPos);
while (!part.empty() && part.back() == '.')
part.pop_back();
// gmail ignores dots
if (domain == "gmail.com")
part.erase(std::remove(part.begin(), part.end(), '.'), part.end());
localPart = part;
}
if (localPart.empty())
return {};
return {{localPart + "@" + domain, domain}};
};
// Warning: must supply already canonicalzied email
inline
std::optional<AccountID>
emailToAccountID(const std::string& canonicalEmail)
{
uint8_t innerHash[SHA512_DIGEST_LENGTH + 4];
SHA512_CTX sha512;
SHA512_Init(&sha512);
SHA512_Update(&sha512, canonicalEmail.c_str(), canonicalEmail.size());
SHA512_Final(innerHash + 4, &sha512);
innerHash[0] = 0xEEU;
innerHash[1] = 0xEEU;
innerHash[2] = 0xFFU;
innerHash[3] = 0xFFU;
{
uint8_t hash[SHA512_DIGEST_LENGTH];
SHA512_CTX sha512;
SHA512_Init(&sha512);
SHA512_Update(&sha512, innerHash, sizeof(innerHash));
SHA512_Final(hash, &sha512);
return AccountID::fromVoid((void*)hash);
}
}
inline
std::optional<EmailDetails>
parseEmail(std::string const& rawEmail, beast::Journal& j)
{
EmailDetails out;
// parse email into headers and body
std::vector<std::string> headers;
std::string body;
{
std::istringstream stream(rawEmail);
std::string line;
while (std::getline(stream, line))
{
if (line.empty() || line == "\r")
break;
// Handle header line continuations
while (stream.peek() == ' ' || stream.peek() == '\t') {
std::string continuation;
std::getline(stream, continuation);
line += '\n' + continuation;
}
if (!line.empty()) {
headers.push_back(line.substr(0, line.size() - (line.back() == '\r' ? 1 : 0)));
}
}
std::ostringstream body_stream;
while (std::getline(stream, line))
body_stream << line << "\n";
body = body_stream.str();
}
// find the from address, canonicalize it and extract the domain
bool foundFrom = false;
bool foundTo = false;
{
static const std::regex
from_regex(R"(^From:\s*(?:.*<)?([^<>\s]+@[^<>\s]+)(?:>)?)", std::regex::icase);
static const std::regex
to_regex(R"(^To:\s*(?:.*<)?([^<>\s]+@[^<>\s]+)(?:>)?)", std::regex::icase);
for (const auto& header : headers)
{
if (foundFrom && foundTo)
break;
std::smatch match;
if (!foundFrom && std::regex_search(header, match, from_regex) && match.size() > 1)
{
auto canon = canonicalizeEmailAddress(match[1].str());
if (!canon)
{
JLOG(j.warn())
<< "EmailAmendment: Cannot parse From address: `"
<< match[1].str() << "`";
return {};
}
out.fromEmail = canon->first;
out.domain = canon->second;
out.from = emailToAccountID(out.fromEmail);
foundFrom = true;
continue;
}
if (std::regex_search(header, match, to_regex) && match.size() > 1)
{
auto canon = canonicalizeEmailAddress(match[1].str());
if (!canon)
{
JLOG(j.warn())
<< "EmailAmendment: Cannot parse To address: `"
<< match[1].str() << "`";
return {};
}
out.toEmail = canon->first;
out.to = emailToAccountID(out.toEmail);
foundTo = true;
continue;
}
}
if (!foundFrom)
{
JLOG(j.warn()) << "EmailAmendment: No From address present in email.";
return {};
}
}
// execution to here means we have:
// 1. Parsed headers and body
// 2. Found a from address and canonicalzied it
// 3. Potentially found a to address and canonicalized it.
// Find instructions
{
static const std::regex
remitPattern(R"(^REMIT (\d+(?:\.\d+)?) ([A-Z]{3})(?:/([r][a-zA-Z0-9]{24,34}))?)");
static const std::regex
rekeyPattern(R"(^REKEY ([r][a-zA-Z0-9]{24,34}))");
std::istringstream stream(body);
std::string line;
out.emailType = EmailType::INVALID;
while (std::getline(stream, line, '\n'))
{
if (!line.empty() && line.back() == '\r')
line.pop_back(); // Remove '\r' if present
std::smatch match;
if (std::regex_match(line, match, remitPattern))
{
try
{
Currency cur;
if (!to_currency(cur, match[2]))
{
JLOG(j.warn()) << "EmailAmendment: Could not parse currency code.";
return {};
}
AccountID issuer = noAccount();
if (match[3].matched)
{
if (isXRP(cur))
{
JLOG(j.warn()) << "EmailAmendment: Native currency cannot specify issuer.";
return {};
}
issuer = decodeBase58Token(match[3], TokenType::AccountID);
if (issuer.empty())
{
JLOG(j.warn()) << "EmailAmendment: Could not parse issuer address.";
return {};
}
}
out.amount = amountFromString({cur, issuer}, match[1]);
}
catch (std::exception const& e)
{
JLOG(j.warn()) << "EmailAmendment: Exception while parsing REMIT. " << e.what();
return {};
}
out.emailType = EmailType::REMIT;
break;
}
if (std::regex_match(line, match, rekeyPattern))
{
AccountID rekey = decodeBase58Token(match[1], TokenType::AccountID);
if (rekey.empty())
{
JLOG(j.warn()) << "EmailAmendment: Could not parse rekey address.";
return {};
}
out.rekey = rekey;
out.emailType = EmailType::REKEY;
break;
}
}
if (out.emailType == EmailType::INVALID)
{
JLOG(j.warn()) << "EmailAmendment: Invalid email type, could not find REMIT or REKEY.";
return{};
}
}
// perform DKIM checks...
// to do this we will use OpenDKIM, and manage it with a smart pointer to prevent
// any leaks from uncommon exit pathways
std::unique<OpenDKIM> odkim;
// perform setup
if (!odkim->setup(j) || !odkim->sane())
return {};
// when odkim goes out of scope it will call the C-apis to destroy the dkim instances
DKIM_STAT status;
DKIM_LIB* dkim_lib = odkim->dkim_lib;
DKIM* dkim = odkim->dkim;
// feed opendkim all headers
{
for (const auto& header : headers)
{
status = dkim_header(dkim, (uint8_t*)header.c_str(), header.length());
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to process header: "
<< dkim_geterror(dkim);
return {};
}
}
status = dkim_eoh(dkim);
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to send end-of-headers"l
return {};
}
}
// feed opendkim email body
{
status = dkim_body(dkim, (uint8_t*)body.c_str(), body.size());
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to process body: "
<< dkim_geterror(dkim);
return {};
}
_Bool testkey;
status = dkim_eom(dkim, &testkey);
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM end-of-message error: "
<< dkim_geterror(dkim);
return {};
}
DKIM_SIGINFO* sig = dkim_getsignature(dkim);
if (!sig)
{
JLOG(j.warn())
<< "EmailAmendment: No DKIM signature found";
return {};
}
if (dkim_sig_getbh(sig) != DKIM_SIGBH_MATCH)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM body hash mismatch";
return {};
}
DKIM_SIGINFO* sig = dkim_getsignature(dkim);
if (!sig)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM signature not found.";
return {};
}
out.dkimDomain =
std::string(reinterpret_cast<char const*>(
reinterpret_cast<void const*>(dkim_sig_getdomain(sig))));
if (out.dkimDomain.empty())
{
JLOG(j.warn())
<< "EmailAmendment: DKIM signature domain empty.";
return {};
}
// RH TODO: decide whether to relax this or not
// strict domain check
if (out.dkimDomain != out.domain)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM domain does not match From address domain.";
return {};
}
}
// execution to here means all checks passed and the instruction was correctly parsed
return out;
}
}

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 71;
static constexpr std::size_t numFeatures = 70;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -358,7 +358,6 @@ extern uint256 const fixXahauV2;
extern uint256 const featureRemit;
extern uint256 const featureZeroB2M;
extern uint256 const fixNSDelete;
extern uint256 const featureEmail;
} // namespace ripple

View File

@@ -30,7 +30,6 @@
#include <ripple/protocol/TxFormats.h>
#include <boost/container/flat_set.hpp>
#include <functional>
#include <ripple/protocol/Email.h>
namespace ripple {

View File

@@ -57,9 +57,8 @@ namespace ripple {
// Universal Transaction flags:
enum UniversalFlags : uint32_t {
tfFullyCanonicalSig = 0x80000000,
tfEmailSig = 0x40000000,
};
constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfEmailSig;
constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig;
constexpr std::uint32_t tfUniversalMask = ~tfUniversal;
// AccountSet flags:

View File

@@ -464,7 +464,6 @@ REGISTER_FIX (fixXahauV2, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(ZeroB2M, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixNSDelete, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Email, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -304,60 +304,9 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
bool const isWildcardNetwork =
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
// email signature flag signals that the txn is authorized
// only by the presence of a DKIM signed email in memos[0]
bool const isEmailSig =
getFlags() & tfEmailSig;
bool validSig = false;
do
try
{
if (isEmailSig)
{
if (!isFieldPresent(sfMemos))
break;
auto const& memos = st.getFieldArray(sfMemos);
auto const& memo = memos[0];
auto memoObj = dynamic_cast<STObject const*>(&memo);
if (!memoObj || (memoObj->getFName() != sfMemo))
break;
bool emailValid = false;
for (auto const& memoElement : *memoObj)
{
auto const& name = memoElement.getFName();
if (name != sfMemoType && name != sfMemoData &&
name != sfMemoFormat)
break;
// The raw data is stored as hex-octets, which we want to decode.
std::optional<Blob> optData = strUnHex(memoElement.getText());
if (!optData)
break;
if (name != sfMemoData)
continue;
std::string const emailContent((char const*)(optData->data()), optData->size());
// RH UPTO
}
}
}
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
@@ -379,8 +328,7 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
{
// Assume it was a signature failure.
validSig = false;
} while (0);
}
if (validSig == false)
return Unexpected("Invalid signature.");
// Signature was verified.

View File

@@ -80,6 +80,7 @@ public:
std::unique_ptr<Manager>
make_Manager(
Section const& section,
beast::insight::Collector::ptr const& collector,
beast::Journal journal);

View File

@@ -89,7 +89,7 @@ struct Entry : public beast::List<Entry>::Node
int refcount;
// Exponentially decaying balance of resource consumption
DecayingSample<decayWindowSeconds, clock_type> local_balance;
DecayingSample<Tuning::getDecayWindowSeconds(), clock_type> local_balance;
// Normalized balance contribution from imports
int remote_balance;

View File

@@ -31,6 +31,7 @@
#include <ripple/resource/Fees.h>
#include <ripple/resource/Gossip.h>
#include <ripple/resource/impl/Import.h>
#include <ripple/resource/impl/Tuning.h>
#include <cassert>
#include <mutex>
@@ -88,11 +89,27 @@ private:
//--------------------------------------------------------------------------
public:
Logic(
Section const& section,
beast::insight::Collector::ptr const& collector,
clock_type& clock,
beast::Journal journal)
: m_stats(collector), m_clock(clock), m_journal(journal)
{
std::uint32_t warningThreshold;
if (get_if_exists(section, "warning_threshold", warningThreshold))
Tuning::warningThreshold = warningThreshold;
std::uint32_t dropThreshold;
if (get_if_exists(section, "drop_threshold", dropThreshold))
Tuning::dropThreshold = dropThreshold;
// std::uint32_t decayWindowSeconds;
// if (get_if_exists(section, "decay_window_seconds", decayWindowSeconds))
// Tuning::decayWindowSeconds = decayWindowSeconds;
std::uint32_t minimumGossipBalance;
if (get_if_exists(section, "minimum_gossip_balance", minimumGossipBalance))
Tuning::minimumGossipBalance = minimumGossipBalance;
}
~Logic()
@@ -200,7 +217,7 @@ public:
Json::Value
getJson()
{
return getJson(warningThreshold);
return getJson(Tuning::warningThreshold);
}
/** Returns a Json::objectValue. */
@@ -266,7 +283,7 @@ public:
{
Gossip::Item item;
item.balance = inboundEntry.local_balance.value(now);
if (item.balance >= minimumGossipBalance)
if (item.balance >= Tuning::minimumGossipBalance)
{
item.address = inboundEntry.key->address;
gossip.items.push_back(item);
@@ -294,7 +311,7 @@ public:
{
// This is a new import
Import& next(resultIt->second);
next.whenExpires = elapsed + gossipExpirationSeconds;
next.whenExpires = elapsed + Tuning::gossipExpirationSeconds;
next.items.reserve(gossip.items.size());
for (auto const& gossipItem : gossip.items)
@@ -312,7 +329,7 @@ public:
// balances and then deduct the old remote balances.
Import next;
next.whenExpires = elapsed + gossipExpirationSeconds;
next.whenExpires = elapsed + Tuning::gossipExpirationSeconds;
next.items.reserve(gossip.items.size());
for (auto const& gossipItem : gossip.items)
{
@@ -387,10 +404,10 @@ public:
static Disposition
disposition(int balance)
{
if (balance >= dropThreshold)
if (balance >= Tuning::dropThreshold)
return Disposition::drop;
if (balance >= warningThreshold)
if (balance >= Tuning::warningThreshold)
return Disposition::warn;
return Disposition::ok;
@@ -437,7 +454,7 @@ public:
break;
}
inactive_.push_back(entry);
entry.whenExpires = m_clock.now() + secondsUntilExpiration;
entry.whenExpires = m_clock.now() + Tuning::secondsUntilExpiration;
}
}
@@ -460,7 +477,7 @@ public:
std::lock_guard _(lock_);
bool notify(false);
auto const elapsed = m_clock.now();
if (entry.balance(m_clock.now()) >= warningThreshold &&
if (entry.balance(m_clock.now()) >= Tuning::warningThreshold &&
elapsed != entry.lastWarningTime)
{
charge(entry, feeWarning);
@@ -485,11 +502,11 @@ public:
bool drop(false);
clock_type::time_point const now(m_clock.now());
int const balance(entry.balance(now));
if (balance >= dropThreshold)
if (balance >= Tuning::dropThreshold)
{
JLOG(m_journal.warn())
<< "Consumer entry " << entry << " dropped with balance "
<< balance << " at or above drop threshold " << dropThreshold;
<< balance << " at or above drop threshold " << Tuning::dropThreshold;
// Adding feeDrop at this point keeps the dropped connection
// from re-connecting for at least a little while after it is

View File

@@ -45,9 +45,10 @@ private:
public:
ManagerImp(
Section const& section,
beast::insight::Collector::ptr const& collector,
beast::Journal journal)
: journal_(journal), logic_(collector, stopwatch(), journal)
: journal_(journal), logic_(section, collector, stopwatch(), journal)
{
thread_ = std::thread{&ManagerImp::run, this};
}
@@ -173,10 +174,11 @@ Manager::~Manager() = default;
std::unique_ptr<Manager>
make_Manager(
Section const& section,
beast::insight::Collector::ptr const& collector,
beast::Journal journal)
{
return std::make_unique<ManagerImp>(collector, journal);
return std::make_unique<ManagerImp>(section, collector, journal);
}
} // namespace Resource

View File

@@ -0,0 +1,34 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
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 AUTHOR 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 "Tuning.h"
namespace ripple {
namespace Resource {
uint32_t Tuning::warningThreshold = 5000;
uint32_t Tuning::dropThreshold = 15000;
// uint32_t Tuning::decayWindowSeconds = 32;
uint32_t Tuning::minimumGossipBalance = 1000;
std::chrono::seconds constexpr Tuning::secondsUntilExpiration{300};
std::chrono::seconds constexpr Tuning::gossipExpirationSeconds{30};
} // namespace Resource
} // namespace ripple

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
// Tuning.h
#ifndef RIPPLE_RESOURCE_TUNING_H_INCLUDED
#define RIPPLE_RESOURCE_TUNING_H_INCLUDED
@@ -25,31 +26,23 @@
namespace ripple {
namespace Resource {
/** Tunable constants. */
enum {
// Balance at which a warning is issued
warningThreshold = 5000
class Tuning
{
public:
static std::uint32_t warningThreshold;
static std::uint32_t dropThreshold;
// static std::uint32_t decayWindowSeconds;
static std::uint32_t minimumGossipBalance;
// Balance at which the consumer is disconnected
,
dropThreshold = 15000
static std::chrono::seconds const secondsUntilExpiration;
static std::chrono::seconds const gossipExpirationSeconds;
// The number of seconds in the exponential decay window
// (This should be a power of two)
,
decayWindowSeconds = 32
// The minimum balance required in order to include a load source in gossip
,
minimumGossipBalance = 1000
static constexpr std::uint32_t getDecayWindowSeconds()
{
return 32;
}
};
// The number of seconds until an inactive table item is removed
std::chrono::seconds constexpr secondsUntilExpiration{300};
// Number of seconds until imported gossip expires
std::chrono::seconds constexpr gossipExpirationSeconds{30};
} // namespace Resource
} // namespace ripple

View File

@@ -61,7 +61,22 @@ getDeliveredAmount(
if (serializedTx->isFieldPresent(sfAmount))
{
return serializedTx->getFieldAmount(sfAmount);
using namespace std::chrono_literals;
// Ledger 4594095 is the first ledger in which the DeliveredAmount field
// was present when a partial payment was made and its absence indicates
// that the amount delivered is listed in the Amount field.
//
// If the ledger closed long after the DeliveredAmount code was deployed
// then its absence indicates that the amount delivered is listed in the
// Amount field. DeliveredAmount went live January 24, 2014.
// 446000000 is in Feb 2014, well after DeliveredAmount went live
if (getLedgerIndex() >= 4594095 ||
getCloseTime() > NetClock::time_point{446000000s} ||
(serializedTx && serializedTx->isFieldPresent(sfNetworkID)))
{
return serializedTx->getFieldAmount(sfAmount);
}
}
return {};

View File

@@ -5466,7 +5466,6 @@ private:
params[jss::transaction] = txIds[i];
auto const jrr = env.rpc("json", "tx", to_string(params));
auto const meta = jrr[jss::result][jss::meta];
BEAST_EXPECT(meta[jss::delivered_amount] == "1000000");
for (auto const& node : meta[sfAffectedNodes.jsonName])
{
auto const nodeType = node[sfLedgerEntryType.jsonName];

View File

@@ -23,6 +23,7 @@
#include <ripple/resource/Consumer.h>
#include <ripple/resource/impl/Entry.h>
#include <ripple/resource/impl/Logic.h>
#include <ripple/resource/impl/Tuning.h>
#include <test/unit_test/SuiteJournal.h>
#include <boost/utility/base_from_member.hpp>
@@ -42,8 +43,8 @@ public:
using clock_type = boost::base_from_member<TestStopwatch>;
public:
explicit TestLogic(beast::Journal journal)
: Logic(beast::insight::NullCollector::New(), member, journal)
explicit TestLogic(Section const& section, beast::Journal journal)
: Logic(section, beast::insight::NullCollector::New(), member, journal)
{
}
@@ -89,9 +90,13 @@ public:
else
testcase("Unlimited warn/drop");
TestLogic logic(j);
auto const config = test::jtx::envconfig([](std::unique_ptr<Config> cfg) {
return cfg;
});
Charge const fee(dropThreshold + 1);
TestLogic logic(config->section("resource"), j);
Charge const fee(Tuning::dropThreshold + 1);
beast::IP::Endpoint const addr(
beast::IP::Endpoint::from_string("192.0.2.2"));
@@ -173,7 +178,7 @@ public:
using namespace std::chrono_literals;
// Give Consumer time to become readmitted. Should never
// exceed expiration time.
auto n = secondsUntilExpiration + 1s;
auto n = Tuning::secondsUntilExpiration + 1s;
while (--n > 0s)
{
++logic.clock();
@@ -199,7 +204,11 @@ public:
{
testcase("Imports");
TestLogic logic(j);
auto const config = test::jtx::envconfig([](std::unique_ptr<Config> cfg) {
return cfg;
});
TestLogic logic(config->section("resource"), j);
Gossip g[5];
@@ -217,7 +226,11 @@ public:
{
testcase("Import");
TestLogic logic(j);
auto const config = test::jtx::envconfig([](std::unique_ptr<Config> cfg) {
return cfg;
});
TestLogic logic(config->section("resource"), j);
Gossip g;
Gossip::Item item;
@@ -236,7 +249,11 @@ public:
{
testcase("Charge");
TestLogic logic(j);
auto const config = test::jtx::envconfig([](std::unique_ptr<Config> cfg) {
return cfg;
});
TestLogic logic(config->section("resource"), j);
{
beast::IP::Endpoint address(
@@ -275,6 +292,41 @@ public:
pass();
}
void
testConfig(beast::Journal j)
{
std::cout << "warningThreshold: " << Tuning::warningThreshold << "\n";
std::cout << "dropThreshold: " << Tuning::dropThreshold << "\n";
std::cout << "getDecayWindowSeconds: " << Tuning::getDecayWindowSeconds() << "\n";
std::cout << "minimumGossipBalance: " << Tuning::minimumGossipBalance << "\n";
BEAST_EXPECT(Tuning::warningThreshold == 5000);
BEAST_EXPECT(Tuning::dropThreshold == 15000);
BEAST_EXPECT(Tuning::getDecayWindowSeconds() == 32);
BEAST_EXPECT(Tuning::minimumGossipBalance == 1000);
BEAST_EXPECT(Tuning::secondsUntilExpiration == std::chrono::seconds{300});
BEAST_EXPECT(Tuning::gossipExpirationSeconds == std::chrono::seconds{30});
auto const config = test::jtx::envconfig([](std::unique_ptr<Config> cfg) {
cfg->section("resource").set("warning_threshold", "15000");
cfg->section("resource").set("drop_threshold", "25000");
cfg->section("resource").set("minimum_gossip_balance", "2000");
cfg->section("resource").set("seconds_until_expiration", "600");
cfg->section("resource").set("gossip_expiration_seconds", "60");
return cfg;
});
TestLogic logic(config->section("resource"), j);
BEAST_EXPECT(Tuning::warningThreshold == 15000);
BEAST_EXPECT(Tuning::dropThreshold == 25000);
BEAST_EXPECT(Tuning::getDecayWindowSeconds() == 32);
BEAST_EXPECT(Tuning::minimumGossipBalance == 2000);
// BEAST_EXPECT(Tuning::secondsUntilExpiration == 600);
// BEAST_EXPECT(Tuning::gossipExpirationSeconds == 60);
}
void
run() override
{
@@ -286,6 +338,7 @@ public:
testCharges(journal);
testImports(journal);
testImport(journal);
testConfig(journal);
}
};

View File

@@ -191,73 +191,80 @@ class DeliveredAmount_test : public beast::unit_test::suite
auto const gw = Account("gateway");
auto const USD = gw["USD"];
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
env.close();
CheckDeliveredAmount checkDeliveredAmount{true};
for (bool const afterSwitchTime : {true, false})
{
// add payments, but do no close until subscribed
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
if (afterSwitchTime)
env.close(NetClock::time_point{446000000s});
else
env.close();
// normal payments
env(pay(gw, alice, USD(50)));
checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess();
// partial payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersPartialPayment();
env.require(balance(bob, USD(1000)));
// failed payment
env(pay(bob, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0)));
}
auto wsc = makeWSClient(env.app().config());
{
Json::Value stream;
// RPC subscribe to ledger stream
stream[jss::streams] = Json::arrayValue;
stream[jss::streams].append("ledger");
stream[jss::accounts] = Json::arrayValue;
stream[jss::accounts].append(toBase58(alice.id()));
stream[jss::accounts].append(toBase58(bob.id()));
stream[jss::accounts].append(toBase58(carol.id()));
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
CheckDeliveredAmount checkDeliveredAmount{afterSwitchTime};
{
BEAST_EXPECT(
jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
// add payments, but do no close until subscribed
// normal payments
env(pay(gw, alice, USD(50)));
checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess();
// partial payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersPartialPayment();
env.require(balance(bob, USD(1000)));
// failed payment
env(pay(bob, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0)));
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
}
{
env.close();
// Check stream update
while (true)
auto wsc = makeWSClient(env.app().config());
{
auto const r = wsc->findMsg(1s, [&](auto const& jv) {
return jv[jss::ledger_index] == 4;
});
if (!r)
break;
if (!r->isMember(jss::transaction))
continue;
BEAST_EXPECT(checkDeliveredAmount.checkTxn(
(*r)[jss::transaction], (*r)[jss::meta]));
Json::Value stream;
// RPC subscribe to ledger stream
stream[jss::streams] = Json::arrayValue;
stream[jss::streams].append("ledger");
stream[jss::accounts] = Json::arrayValue;
stream[jss::accounts].append(toBase58(alice.id()));
stream[jss::accounts].append(toBase58(bob.id()));
stream[jss::accounts].append(toBase58(carol.id()));
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(
jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(
jv.isMember(jss::ripplerpc) &&
jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
}
{
env.close();
// Check stream update
while (true)
{
auto const r = wsc->findMsg(1s, [&](auto const& jv) {
return jv[jss::ledger_index] == 4;
});
if (!r)
break;
if (!r->isMember(jss::transaction))
continue;
BEAST_EXPECT(checkDeliveredAmount.checkTxn(
(*r)[jss::transaction], (*r)[jss::meta]));
}
}
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
}
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
}
void
testTxDeliveredAmountRPC(FeatureBitset features)
@@ -273,41 +280,49 @@ class DeliveredAmount_test : public beast::unit_test::suite
auto const gw = Account("gateway");
auto const USD = gw["USD"];
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
env.close();
for (bool const afterSwitchTime : {true, false})
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
if (afterSwitchTime)
env.close(NetClock::time_point{446000000s});
else
env.close();
CheckDeliveredAmount checkDeliveredAmount{true};
// normal payments
env(pay(gw, alice, USD(50)));
checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess();
CheckDeliveredAmount checkDeliveredAmount{afterSwitchTime};
// normal payments
env(pay(gw, alice, USD(50)));
checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess();
// partial payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersPartialPayment();
env.require(balance(bob, USD(1000)));
// partial payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersPartialPayment();
env.require(balance(bob, USD(1000)));
// failed payment
env(pay(gw, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0)));
// failed payment
env(pay(gw, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0)));
env.close();
std::string index;
Json::Value jvParams;
jvParams[jss::ledger_index] = 4u;
jvParams[jss::transactions] = true;
jvParams[jss::expand] = true;
auto const jtxn = env.rpc(
"json",
"ledger",
to_string(jvParams))[jss::result][jss::ledger][jss::transactions];
for (auto const& t : jtxn)
BEAST_EXPECT(checkDeliveredAmount.checkTxn(t, t[jss::metaData]));
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
env.close();
std::string index;
Json::Value jvParams;
jvParams[jss::ledger_index] = 4u;
jvParams[jss::transactions] = true;
jvParams[jss::expand] = true;
auto const jtxn = env.rpc(
"json",
"ledger",
to_string(
jvParams))[jss::result][jss::ledger][jss::transactions];
for (auto const& t : jtxn)
BEAST_EXPECT(
checkDeliveredAmount.checkTxn(t, t[jss::metaData]));
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
}
}
public:

View File

@@ -276,11 +276,11 @@ class NoRippleCheckLimits_test : public beast::unit_test::suite
Endpoint::from_string(test::getEnvLocalhostAddr()));
// if we go above the warning threshold, reset
if (c.balance() > warningThreshold)
if (c.balance() > Tuning::warningThreshold)
{
using ct = beast::abstract_clock<steady_clock>;
c.entry().local_balance =
DecayingSample<decayWindowSeconds, ct>{steady_clock::now()};
DecayingSample<Tuning::getDecayWindowSeconds(), ct>{steady_clock::now()};
}
};

View File

@@ -57,6 +57,7 @@ public:
{
Env env(*this);
auto const result = env.rpc("server_definitions");
std::cout << "RESULT: " << result << "\n";
BEAST_EXPECT(!result[jss::result].isMember(jss::error));
BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS));
BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES));