Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Holland
256f84b4de first partial draft of featureEmail, not finished 2024-08-12 14:23:50 +10:00
14 changed files with 581 additions and 107 deletions

View File

@@ -9,15 +9,6 @@ echo "-- GITHUB_RUN_NUMBER: $4"
umask 0000;
echo "Fixing CentOS 7 EOL"
sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
yum clean all
yum-config-manager --disable centos-sclo-sclo
####
cd /io;
mkdir src/certs;
curl --silent -k https://raw.githubusercontent.com/RichardAH/rippled-release-builder/main/ca-bundle/certbundle.h -o src/certs/certbundle.h;

View File

@@ -1142,12 +1142,8 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
// Enforce Network bar for emitted txn
if (view->rules().enabled(featureHooks) && hook::isEmittedTxn(*iTrans))
{
// RH NOTE: Warning removed here due to ConsesusSet using this function
// which continually triggers this bar. Doesn't seem dangerous, just
// annoying.
// JLOG(m_journal.warn())
// << "Submitted transaction invalid: EmitDetails present.";
JLOG(m_journal.warn())
<< "Submitted transaction invalid: EmitDetails present.";
return;
}
@@ -1159,11 +1155,7 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
if ((flags & SF_BAD) != 0)
{
// RH NOTE: Warning removed here due to ConsesusSet using this function
// which continually triggers this bar. Doesn't seem dangerous, just
// annoying.
// JLOG(m_journal.warn()) << "Submitted transaction cached bad";
JLOG(m_journal.warn()) << "Submitted transaction cached bad";
return;
}

View File

@@ -173,11 +173,6 @@ updateLedgerDBs(
auto const sParentHash{to_string(ledger->info().parentHash)};
auto const sDrops{to_string(ledger->info().drops)};
auto const closingTime{
ledger->info().closeTime.time_since_epoch().count()};
auto const prevClosingTime{
ledger->info().parentCloseTime.time_since_epoch().count()};
auto const closeTimeRes{ledger->info().closeTimeResolution.count()};
auto const sAccountHash{to_string(ledger->info().accountHash)};
auto const sTxHash{to_string(ledger->info().txHash)};
@@ -193,8 +188,11 @@ updateLedgerDBs(
":closingTime, :prevClosingTime, :closeTimeRes,"
":closeFlags, :accountSetHash, :transSetHash);",
soci::use(sHash), soci::use(ledgerSeq), soci::use(sParentHash),
soci::use(sDrops), soci::use(closingTime),
soci::use(prevClosingTime), soci::use(closeTimeRes),
soci::use(sDrops),
soci::use(ledger->info().closeTime.time_since_epoch().count()),
soci::use(
ledger->info().parentCloseTime.time_since_epoch().count()),
soci::use(ledger->info().closeTimeResolution.count()),
soci::use(ledger->info().closeFlags), soci::use(sAccountHash),
soci::use(sTxHash);

View File

@@ -205,20 +205,19 @@ insertPeerReservation(
PublicKey const& nodeId,
std::string const& description)
{
auto const sNodeId = toBase58(TokenType::NodePublic, nodeId);
session << "INSERT INTO PeerReservations (PublicKey, Description) "
"VALUES (:nodeId, :desc) "
"ON CONFLICT (PublicKey) DO UPDATE SET "
"Description=excluded.Description",
soci::use(sNodeId), soci::use(description);
soci::use(toBase58(TokenType::NodePublic, nodeId)),
soci::use(description);
}
void
deletePeerReservation(soci::session& session, PublicKey const& nodeId)
{
auto const sNodeId = toBase58(TokenType::NodePublic, nodeId);
session << "DELETE FROM PeerReservations WHERE PublicKey = :nodeId",
soci::use(sNodeId);
soci::use(toBase58(TokenType::NodePublic, nodeId));
}
bool

View File

@@ -1921,12 +1921,6 @@ Transactor::operator()()
STObject const meta = metaRaw.getAsObject();
uint32_t lgrCur = view().seq();
bool const has240819 = view().rules().enabled(fix240819);
auto const& sfRewardFields =
*(ripple::SField::knownCodeToField.at(917511 - has240819));
// iterate all affected balances
for (auto const& node : meta.getFieldArray(sfAffectedNodes))
{
@@ -1938,7 +1932,7 @@ Transactor::operator()()
if (nodeType != ltACCOUNT_ROOT || metaType == sfDeletedNode)
continue;
if (!node.isFieldPresent(sfRewardFields) ||
if (!node.isFieldPresent(sfFinalFields) ||
!node.isFieldPresent(sfLedgerIndex))
continue;
@@ -1954,7 +1948,7 @@ Transactor::operator()()
continue;
STObject& finalFields = (const_cast<STObject&>(node))
.getField(sfRewardFields)
.getField(sfFinalFields)
.downcast<STObject>();
if (!finalFields.isFieldPresent(sfBalance))

View File

@@ -127,7 +127,7 @@ sizedItems
{SizedItem::txnDBCache, {{ 4, 12, 24, 64, 128 }}},
{SizedItem::lgrDBCache, {{ 4, 8, 16, 32, 128 }}},
{SizedItem::openFinalLimit, {{ 8, 16, 32, 64, 128 }}},
{SizedItem::burstSize, {{ 4, 8, 16, 32, 64*1024*1024 }}},
{SizedItem::burstSize, {{ 4, 8, 16, 32, 48 }}},
{SizedItem::ramSizeGB, {{ 8, 12, 16, 24, 32 }}},
{SizedItem::accountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}}
}};

493
src/ripple/protocol/Email.h Normal file
View File

@@ -0,0 +1,493 @@
#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

@@ -358,7 +358,7 @@ extern uint256 const fixXahauV2;
extern uint256 const featureRemit;
extern uint256 const featureZeroB2M;
extern uint256 const fixNSDelete;
extern uint256 const fix240819;
extern uint256 const featureEmail;
} // namespace ripple

View File

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

View File

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

View File

@@ -464,7 +464,7 @@ 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_FIX (fix240819, Supported::yes, VoteBehavior::DefaultYes);
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,9 +304,60 @@ 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);
@@ -328,7 +379,8 @@ 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

@@ -256,8 +256,6 @@ BaseWSPeer<Handler, Impl>::close(
return post(strand_, [self = impl().shared_from_this(), reason] {
self->close(reason);
});
if (do_close_)
return;
do_close_ = true;
if (wq_.empty())
{

View File

@@ -3968,8 +3968,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
using namespace std::chrono_literals;
testcase("test claim reward valid without unl report");
Env env{*this, envconfig(), features - featureXahauGenesis};
bool const has240819 = env.current()->rules().enabled(fix240819);
Env env{
*this, envconfig(), supported_amendments() - featureXahauGenesis};
double const rateDrops = 0.00333333333 * 1'000'000;
STAmount const feesXRP = XRP(1);
@@ -4050,12 +4050,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
env(claimReward(user, env.master), fee(feesXRP), ter(tecHOOK_REJECTED));
env.close();
@@ -4100,12 +4095,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser1 = preUser1 + netReward1;
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger1,
preLedger1 + 1,
has240819 ? (preUser1 - feesXRP) : postUser1,
preTime1));
env, user, preLedger1, preLedger1 + 1, postUser1, preTime1));
}
void
@@ -4229,14 +4219,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
}
void
@@ -4368,15 +4352,10 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postAlice = preAlice + netReward + l1Reward;
bool const boolResult = withXahauV1 ? true : false;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(
expectAccountFields(
env,
alice,
preLedger,
preLedger + 1,
has240819 ? (preAlice - feesXRP) : postAlice,
preTime) == boolResult);
env, alice, preLedger, preLedger + 1, postAlice, preTime) ==
boolResult);
}
}
@@ -4388,7 +4367,6 @@ struct XahauGenesis_test : public beast::unit_test::suite
testcase("test claim reward optin optout");
Env env{*this, envconfig(), features - featureXahauGenesis};
bool const has240819 = env.current()->rules().enabled(fix240819);
double const rateDrops = 0.00333333333 * 1'000'000;
STAmount const feesXRP = XRP(1);
@@ -4458,12 +4436,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
// opt out of claim rewards
env(claimReward(user, std::nullopt, 1), fee(feesXRP), ter(tesSUCCESS));
@@ -4488,7 +4461,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
user,
preLedger1,
preLedger1 + 1,
has240819 ? (env.balance(user) + feesXRP) : env.balance(user),
env.balance(user),
preTime1));
}
@@ -4570,14 +4543,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
has240819 ? preLedger : preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
}
void
@@ -4651,14 +4618,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
}
void
@@ -4863,13 +4824,13 @@ struct XahauGenesis_test : public beast::unit_test::suite
Env env{
*this,
makeGenesisConfig(
features - featureXahauGenesis,
supported_amendments() - featureXahauGenesis,
21337,
"10",
"1000000",
"200000",
0),
features - featureXahauGenesis};
supported_amendments() - featureXahauGenesis};
STAmount const feesXRP = XRP(1);
@@ -4929,7 +4890,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
using namespace std::chrono_literals;
testcase("test compound interest over 12 claims");
Env env{*this, envconfig(), features - featureXahauGenesis};
Env env{
*this, envconfig(), supported_amendments() - featureXahauGenesis};
double const rateDrops = 0.00333333333 * 1'000'000;
STAmount const feesXRP = XRP(1);
@@ -5003,14 +4965,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
env, user, preLedger, preLedger + 1, postUser, preTime));
}
STAmount const endBal = env.balance(user);
@@ -5056,7 +5012,6 @@ struct XahauGenesis_test : public beast::unit_test::suite
using namespace test::jtx;
auto const sa = supported_amendments();
testGovernHookWithFeats(sa);
testRewardHookWithFeats(sa - fix240819);
testRewardHookWithFeats(sa);
}
};