XRPFees: Fee setting and handling improvements (#4247)

* Introduces amendment `XRPFees`
* Convert fee voting and protocol messages to use XRPAmounts
* Includes Validations, Change transactions, the "Fees" ledger object,
  and subscription messages

* Improve handling of 0 drop reference fee with TxQ. For use with networks that do not want to require fees
* Note that fee escalation logic is still in place, which may cause the
  open ledger fee to rise if the network is busy. 0 drop transactions
  will still queue, and fee escalation can be effectively disabled by
  modifying the configuration on all nodes

* Change default network reserves to match Mainnet

* Name the new SFields *Drops (not *XRP)
* Reserve SField IDs for Hooks

* Clarify comments explaining the ttFEE transaction field validation
This commit is contained in:
Ed Hennis
2023-02-02 16:20:35 -08:00
committed by GitHub
parent 0ce15e0e35
commit e4b17d1cf2
46 changed files with 691 additions and 337 deletions

View File

@@ -839,7 +839,8 @@ RCLConsensus::Adaptor::validate(
if (ledger.ledger_->isVotingLedger())
{
// Fees:
feeVote_->doValidation(ledger.ledger_->fees(), v);
feeVote_->doValidation(
ledger.ledger_->fees(), ledger.ledger_->rules(), v);
// Amendments
// FIXME: pass `v` and have the function insert the array

View File

@@ -591,29 +591,9 @@ Ledger::setup(Config const& config)
{
bool ret = true;
fees_.base = config.FEE_DEFAULT;
fees_.units = config.TRANSACTION_FEE_BASE;
fees_.reserve = config.FEE_ACCOUNT_RESERVE;
fees_.increment = config.FEE_OWNER_RESERVE;
try
{
if (auto const sle = read(keylet::fees()))
{
// VFALCO NOTE Why getFieldIndex and not isFieldPresent?
if (sle->getFieldIndex(sfBaseFee) != -1)
fees_.base = sle->getFieldU64(sfBaseFee);
if (sle->getFieldIndex(sfReferenceFeeUnits) != -1)
fees_.units = sle->getFieldU32(sfReferenceFeeUnits);
if (sle->getFieldIndex(sfReserveBase) != -1)
fees_.reserve = sle->getFieldU32(sfReserveBase);
if (sle->getFieldIndex(sfReserveIncrement) != -1)
fees_.increment = sle->getFieldU32(sfReserveIncrement);
}
rules_ = makeRulesGivenLedger(*this, config.features);
}
catch (SHAMapMissingNode const&)
{
@@ -624,9 +604,56 @@ Ledger::setup(Config const& config)
Rethrow();
}
fees_.base = config.FEE_DEFAULT;
fees_.reserve = config.FEE_ACCOUNT_RESERVE;
fees_.increment = config.FEE_OWNER_RESERVE;
try
{
rules_ = makeRulesGivenLedger(*this, config.features);
if (auto const sle = read(keylet::fees()))
{
bool oldFees = false;
bool newFees = false;
{
auto const baseFee = sle->at(~sfBaseFee);
auto const reserveBase = sle->at(~sfReserveBase);
auto const reserveIncrement = sle->at(~sfReserveIncrement);
if (baseFee)
fees_.base = *baseFee;
if (reserveBase)
fees_.reserve = *reserveBase;
if (reserveIncrement)
fees_.increment = *reserveIncrement;
oldFees = baseFee || reserveBase || reserveIncrement;
}
{
auto const baseFeeXRP = sle->at(~sfBaseFeeDrops);
auto const reserveBaseXRP = sle->at(~sfReserveBaseDrops);
auto const reserveIncrementXRP =
sle->at(~sfReserveIncrementDrops);
auto assign = [&ret](
XRPAmount& dest,
std::optional<STAmount> const& src) {
if (src)
{
if (src->native())
dest = src->xrp();
else
ret = false;
}
};
assign(fees_.base, baseFeeXRP);
assign(fees_.reserve, reserveBaseXRP);
assign(fees_.increment, reserveIncrementXRP);
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
}
if (oldFees && newFees)
// Should be all of one or the other, but not both
ret = false;
if (!rules_.enabled(featureXRPFees) && newFees)
// Can't populate the new fees before the amendment is enabled
ret = false;
}
}
catch (SHAMapMissingNode const&)
{

View File

@@ -42,9 +42,6 @@ public:
/** The cost of a reference transaction in drops. */
XRPAmount reference_fee{10};
/** The cost of a reference transaction in fee units. */
static constexpr FeeUnit32 reference_fee_units{10};
/** The account reserve requirement in drops. */
XRPAmount account_reserve{10 * DROPS_PER_XRP};
@@ -60,7 +57,10 @@ public:
@param baseValidation
*/
virtual void
doValidation(Fees const& lastFees, STValidation& val) = 0;
doValidation(
Fees const& lastFees,
Rules const& rules,
STValidation& val) = 0;
/** Cast our local vote on the fee.

View File

@@ -33,51 +33,57 @@ class VotableValue
{
private:
using value_type = XRPAmount;
value_type const mCurrent; // The current setting
value_type const mTarget; // The setting we want
std::map<value_type, int> mVoteMap;
value_type const current_; // The current setting
value_type const target_; // The setting we want
std::map<value_type, int> voteMap_;
public:
VotableValue(value_type current, value_type target)
: mCurrent(current), mTarget(target)
: current_(current), target_(target)
{
// Add our vote
++mVoteMap[mTarget];
++voteMap_[target_];
}
void
addVote(value_type vote)
{
++mVoteMap[vote];
++voteMap_[vote];
}
void
noVote()
{
addVote(mCurrent);
addVote(current_);
}
value_type
current() const
{
return current_;
}
std::pair<value_type, bool>
getVotes() const;
};
auto
VotableValue::getVotes() const -> value_type
VotableValue::getVotes() const -> std::pair<value_type, bool>
{
value_type ourVote = mCurrent;
value_type ourVote = current_;
int weight = 0;
for (auto const& [key, val] : mVoteMap)
for (auto const& [key, val] : voteMap_)
{
// Take most voted value between current and target, inclusive
if ((key <= std::max(mTarget, mCurrent)) &&
(key >= std::min(mTarget, mCurrent)) && (val > weight))
if ((key <= std::max(target_, current_)) &&
(key >= std::min(target_, current_)) && (val > weight))
{
ourVote = key;
weight = val;
}
}
return ourVote;
return {ourVote, ourVote != current_};
}
} // namespace detail
@@ -94,7 +100,8 @@ public:
FeeVoteImpl(Setup const& setup, beast::Journal journal);
void
doValidation(Fees const& lastFees, STValidation& val) override;
doValidation(Fees const& lastFees, Rules const& rules, STValidation& val)
override;
void
doVoting(
@@ -111,36 +118,78 @@ FeeVoteImpl::FeeVoteImpl(Setup const& setup, beast::Journal journal)
}
void
FeeVoteImpl::doValidation(Fees const& lastFees, STValidation& v)
FeeVoteImpl::doValidation(
Fees const& lastFees,
Rules const& rules,
STValidation& v)
{
// Values should always be in a valid range (because the voting process
// will ignore out-of-range values) but if we detect such a case, we do
// not send a value.
if (lastFees.base != target_.reference_fee)
if (rules.enabled(featureXRPFees))
{
auto vote = [&v, this](
auto const current,
XRPAmount target,
const char* name,
auto const& sfield) {
if (current != target)
{
JLOG(journal_.info())
<< "Voting for base fee of " << target_.reference_fee;
<< "Voting for " << name << " of " << target;
if (auto const f = target_.reference_fee.dropsAs<std::uint64_t>())
v.setFieldU64(sfBaseFee, *f);
v[sfield] = target;
}
if (lastFees.accountReserve(0) != target_.account_reserve)
{
JLOG(journal_.info())
<< "Voting for base reserve of " << target_.account_reserve;
if (auto const f = target_.account_reserve.dropsAs<std::uint32_t>())
v.setFieldU32(sfReserveBase, *f);
};
vote(lastFees.base, target_.reference_fee, "base fee", sfBaseFeeDrops);
vote(
lastFees.accountReserve(0),
target_.account_reserve,
"base reserve",
sfReserveBaseDrops);
vote(
lastFees.increment,
target_.owner_reserve,
"reserve increment",
sfReserveIncrementDrops);
}
if (lastFees.increment != target_.owner_reserve)
else
{
auto to32 = [](XRPAmount target) {
return target.dropsAs<std::uint32_t>();
};
auto to64 = [](XRPAmount target) {
return target.dropsAs<std::uint64_t>();
};
auto vote = [&v, this](
auto const current,
XRPAmount target,
auto const& convertCallback,
const char* name,
auto const& sfield) {
if (current != target)
{
JLOG(journal_.info())
<< "Voting for reserve increment of " << target_.owner_reserve;
<< "Voting for " << name << " of " << target;
if (auto const f = target_.owner_reserve.dropsAs<std::uint32_t>())
v.setFieldU32(sfReserveIncrement, *f);
if (auto const f = convertCallback(target))
v[sfield] = *f;
}
};
vote(lastFees.base, target_.reference_fee, to64, "base fee", sfBaseFee);
vote(
lastFees.accountReserve(0),
target_.account_reserve,
to32,
"base reserve",
sfReserveBase);
vote(
lastFees.increment,
target_.owner_reserve,
to32,
"reserve increment",
sfReserveIncrement);
}
}
@@ -151,7 +200,7 @@ FeeVoteImpl::doVoting(
std::shared_ptr<SHAMap> const& initialPosition)
{
// LCL must be flag ledger
assert(isFlagLedger(lastClosedLedger->seq()));
assert(lastClosedLedger && isFlagLedger(lastClosedLedger->seq()));
detail::VotableValue baseFeeVote(
lastClosedLedger->fees().base, target_.reference_fee);
@@ -162,79 +211,110 @@ FeeVoteImpl::doVoting(
detail::VotableValue incReserveVote(
lastClosedLedger->fees().increment, target_.owner_reserve);
auto const& rules = lastClosedLedger->rules();
if (rules.enabled(featureXRPFees))
{
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue& value,
SF_AMOUNT const& xrpField) {
if (auto const field = ~val->at(~xrpField);
field && field->native())
{
auto const vote = field->xrp();
if (isLegalAmountSigned(vote))
value.addVote(vote);
else
value.noVote();
}
else
{
value.noVote();
}
};
for (auto const& val : set)
{
if (val->isTrusted())
if (!val->isTrusted())
continue;
doVote(val, baseFeeVote, sfBaseFeeDrops);
doVote(val, baseReserveVote, sfReserveBaseDrops);
doVote(val, incReserveVote, sfReserveIncrementDrops);
}
}
else
{
if (val->isFieldPresent(sfBaseFee))
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue& value,
auto const& valueField) {
if (auto const field = val->at(~valueField))
{
using xrptype = XRPAmount::value_type;
auto const vote = val->getFieldU64(sfBaseFee);
auto const vote = *field;
if (vote <= std::numeric_limits<xrptype>::max() &&
isLegalAmount(XRPAmount{unsafe_cast<xrptype>(vote)}))
baseFeeVote.addVote(
isLegalAmountSigned(XRPAmount{unsafe_cast<xrptype>(vote)}))
value.addVote(
XRPAmount{unsafe_cast<XRPAmount::value_type>(vote)});
else
// Invalid amounts will be treated as if they're
// not provided. Don't throw because this value is
// provided by an external entity.
baseFeeVote.noVote();
value.noVote();
}
else
{
baseFeeVote.noVote();
value.noVote();
}
};
if (val->isFieldPresent(sfReserveBase))
for (auto const& val : set)
{
baseReserveVote.addVote(
XRPAmount{val->getFieldU32(sfReserveBase)});
}
else
{
baseReserveVote.noVote();
}
if (val->isFieldPresent(sfReserveIncrement))
{
incReserveVote.addVote(
XRPAmount{val->getFieldU32(sfReserveIncrement)});
}
else
{
incReserveVote.noVote();
}
if (!val->isTrusted())
continue;
doVote(val, baseFeeVote, sfBaseFee);
doVote(val, baseReserveVote, sfReserveBase);
doVote(val, incReserveVote, sfReserveIncrement);
}
}
// choose our positions
// If any of the values are invalid, send the current values.
auto const baseFee = baseFeeVote.getVotes().dropsAs<std::uint64_t>(
lastClosedLedger->fees().base);
auto const baseReserve = baseReserveVote.getVotes().dropsAs<std::uint32_t>(
lastClosedLedger->fees().accountReserve(0));
auto const incReserve = incReserveVote.getVotes().dropsAs<std::uint32_t>(
lastClosedLedger->fees().increment);
constexpr FeeUnit32 feeUnits = Setup::reference_fee_units;
// TODO: Use structured binding once LLVM issue
// https://github.com/llvm/llvm-project/issues/48582
// is fixed.
auto const baseFee = baseFeeVote.getVotes();
auto const baseReserve = baseReserveVote.getVotes();
auto const incReserve = incReserveVote.getVotes();
auto const seq = lastClosedLedger->info().seq + 1;
// add transactions to our position
if ((baseFee != lastClosedLedger->fees().base) ||
(baseReserve != lastClosedLedger->fees().accountReserve(0)) ||
(incReserve != lastClosedLedger->fees().increment))
if (baseFee.second || baseReserve.second || incReserve.second)
{
JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee
<< "/" << baseReserve << "/" << incReserve;
JLOG(journal_.warn())
<< "We are voting for a fee change: " << baseFee.first << "/"
<< baseReserve.first << "/" << incReserve.first;
STTx feeTx(
ttFEE,
[seq, baseFee, baseReserve, incReserve, feeUnits](auto& obj) {
STTx feeTx(ttFEE, [=, &rules](auto& obj) {
obj[sfAccount] = AccountID();
obj[sfLedgerSequence] = seq;
obj[sfBaseFee] = baseFee;
obj[sfReserveBase] = baseReserve;
obj[sfReserveIncrement] = incReserve;
obj[sfReferenceFeeUnits] = feeUnits.fee();
if (rules.enabled(featureXRPFees))
{
obj[sfBaseFeeDrops] = baseFee.first;
obj[sfReserveBaseDrops] = baseReserve.first;
obj[sfReserveIncrementDrops] = incReserve.first;
}
else
{
// Without the featureXRPFees amendment, these fields are
// required.
obj[sfBaseFee] =
baseFee.first.dropsAs<std::uint64_t>(baseFeeVote.current());
obj[sfReserveBase] = baseReserve.first.dropsAs<std::uint32_t>(
baseReserveVote.current());
obj[sfReserveIncrement] =
incReserve.first.dropsAs<std::uint32_t>(
incReserveVote.current());
obj[sfReferenceFeeUnits] = Config::FEE_UNITS_DEPRECATED;
}
});
uint256 txID = feeTx.getTransactionID();

View File

@@ -161,7 +161,7 @@ private:
// Scale using load as well as base rate
XRPAmount
scaleFeeLoad(
FeeUnit64 fee,
XRPAmount fee,
LoadFeeTrack const& feeTrack,
Fees const& fees,
bool bUnlimited);

View File

@@ -2171,15 +2171,30 @@ NetworkOPsImp::pubValidation(std::shared_ptr<STValidation> const& val)
if (auto const loadFee = (*val)[~sfLoadFee])
jvObj[jss::load_fee] = *loadFee;
if (auto const baseFee = (*val)[~sfBaseFee])
if (auto const baseFee = val->at(~sfBaseFee))
jvObj[jss::base_fee] = static_cast<double>(*baseFee);
if (auto const reserveBase = (*val)[~sfReserveBase])
if (auto const reserveBase = val->at(~sfReserveBase))
jvObj[jss::reserve_base] = *reserveBase;
if (auto const reserveInc = (*val)[~sfReserveIncrement])
if (auto const reserveInc = val->at(~sfReserveIncrement))
jvObj[jss::reserve_inc] = *reserveInc;
// (The ~ operator converts the Proxy to a std::optional, which
// simplifies later operations)
if (auto const baseFeeXRP = ~val->at(~sfBaseFeeDrops);
baseFeeXRP && baseFeeXRP->native())
jvObj[jss::base_fee_drops] = baseFeeXRP->xrp().jsonClipped();
if (auto const reserveBaseXRP = ~val->at(~sfReserveBaseDrops);
reserveBaseXRP && reserveBaseXRP->native())
jvObj[jss::reserve_base_drops] =
reserveBaseXRP->xrp().jsonClipped();
if (auto const reserveIncXRP = ~val->at(~sfReserveIncrementDrops);
reserveIncXRP && reserveIncXRP->native())
jvObj[jss::reserve_inc_drops] = reserveIncXRP->xrp().jsonClipped();
for (auto i = mStreamMaps[sValidations].begin();
i != mStreamMaps[sValidations].end();)
{
@@ -2883,7 +2898,8 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
jvObj[jss::ledger_time] = Json::Value::UInt(
lpAccepted->info().closeTime.time_since_epoch().count());
jvObj[jss::fee_ref] = lpAccepted->fees().units.jsonClipped();
if (!lpAccepted->rules().enabled(featureXRPFees))
jvObj[jss::fee_ref] = Config::FEE_UNITS_DEPRECATED;
jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped();
jvObj[jss::reserve_base] =
lpAccepted->fees().accountReserve(0).jsonClipped();
@@ -3889,7 +3905,8 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, Json::Value& jvResult)
jvResult[jss::ledger_hash] = to_string(lpClosed->info().hash);
jvResult[jss::ledger_time] = Json::Value::UInt(
lpClosed->info().closeTime.time_since_epoch().count());
jvResult[jss::fee_ref] = lpClosed->fees().units.jsonClipped();
if (!lpClosed->rules().enabled(featureXRPFees))
jvResult[jss::fee_ref] = Config::FEE_UNITS_DEPRECATED;
jvResult[jss::fee_base] = lpClosed->fees().base.jsonClipped();
jvResult[jss::reserve_base] =
lpClosed->fees().accountReserve(0).jsonClipped();

View File

@@ -860,7 +860,7 @@ setup_TxQ(Config const&);
template <class T>
XRPAmount
toDrops(FeeLevel<T> const& level, XRPAmount const& baseFee)
toDrops(FeeLevel<T> const& level, XRPAmount baseFee)
{
if (auto const drops = mulDiv(level, baseFee, TxQ::baseLevel); drops.first)
return drops.second;

View File

@@ -87,30 +87,13 @@ LoadFeeTrack::lowerLocalFee()
// Scale using load as well as base rate
XRPAmount
scaleFeeLoad(
FeeUnit64 fee,
XRPAmount fee,
LoadFeeTrack const& feeTrack,
Fees const& fees,
bool bUnlimited)
{
if (fee == 0)
return XRPAmount{0};
// Normally, types with different units wouldn't be mathematically
// compatible. This function is an exception.
auto lowestTerms = [](auto& a, auto& b) {
auto value = [](auto val) {
if constexpr (std::is_arithmetic_v<decltype(val)>)
return val;
else
return val.value();
};
if (auto const g = std::gcd(value(a), value(b)))
{
a = value(a) / g;
b = value(b) / g;
}
};
return fee;
// Collect the fee rates
auto [feeFactor, uRemFee] = feeTrack.getScalingFactors();
@@ -120,45 +103,12 @@ scaleFeeLoad(
if (bUnlimited && (feeFactor > uRemFee) && (feeFactor < (4 * uRemFee)))
feeFactor = uRemFee;
XRPAmount baseFee{fees.base};
// Compute:
// fee = fee * baseFee * feeFactor / (fees.units * lftNormalFee);
// fee = fee * feeFactor / (lftNormalFee);
// without overflow, and as accurately as possible
// The denominator of the fraction we're trying to compute.
// fees.units and lftNormalFee are both 32 bit,
// so the multiplication can't overflow.
auto den = FeeUnit64{fees.units} *
safe_cast<std::uint64_t>(feeTrack.getLoadBase());
// Reduce fee * baseFee * feeFactor / (fees.units * lftNormalFee)
// to lowest terms.
lowestTerms(fee, den);
lowestTerms(baseFee, den);
lowestTerms(feeFactor, den);
// fee and baseFee are 64 bit, feeFactor is 32 bit
// Order fee and baseFee largest first
// Normally, these types wouldn't be comparable or swappable.
// This function is an exception.
if (fee.value() < baseFee.value())
{
auto tmp = fee.value();
fee = baseFee.value();
baseFee = tmp;
}
// double check
assert(fee.value() >= baseFee.value());
// If baseFee * feeFactor overflows, the final result will overflow
XRPAmount const baseFeeOverflow{
std::numeric_limits<XRPAmount::value_type>::max() / feeFactor};
if (baseFee > baseFeeOverflow)
{
Throw<std::overflow_error>("scaleFeeLoad");
}
baseFee *= feeFactor;
auto const result = mulDiv(fee, baseFee, den);
auto const result = mulDiv(
fee, feeFactor, safe_cast<std::uint64_t>(feeTrack.getLoadBase()));
if (!result.first)
Throw<std::overflow_error>("scaleFeeLoad");
return result.second;

View File

@@ -38,14 +38,18 @@ static FeeLevel64
getFeeLevelPaid(ReadView const& view, STTx const& tx)
{
auto const [baseFee, effectiveFeePaid] = [&view, &tx]() {
XRPAmount baseFee = view.fees().toDrops(calculateBaseFee(view, tx));
XRPAmount baseFee = calculateBaseFee(view, tx);
XRPAmount feePaid = tx[sfFee].xrp();
// If baseFee is 0 then the cost of a basic transaction is free.
XRPAmount const ref = baseFee.signum() > 0
? XRPAmount{0}
: calculateDefaultBaseFee(view, tx);
return std::pair{baseFee + ref, feePaid + ref};
// If baseFee is 0 then the cost of a basic transaction is free, but we
// need the effective fee level to be non-zero.
XRPAmount const mod = [&view, &tx, baseFee]() {
if (baseFee.signum() > 0)
return XRPAmount{0};
auto def = calculateDefaultBaseFee(view, tx);
return def.signum() == 0 ? XRPAmount{1} : def;
}();
return std::pair{baseFee + mod, feePaid + mod};
}();
assert(baseFee.signum() > 0);
@@ -1072,19 +1076,27 @@ TxQ::apply(
LastLedgerSeq and MaybeTx::retriesRemaining.
*/
auto const balance = (*sleAccount)[sfBalance].xrp();
/* Get the minimum possible reserve. If fees exceed
/* Get the minimum possible account reserve. If it
is at least 10 * the base fee, and fees exceed
this amount, the transaction can't be queued.
Considering that typical fees are several orders
Currently typical fees are several orders
of magnitude smaller than any current or expected
future reserve, this calculation is simpler than
future reserve. This calculation is simpler than
trying to figure out the potential changes to
the ownerCount that may occur to the account
as a result of these transactions, and removes
any need to account for other transactions that
may affect the owner count while these are queued.
However, in case the account reserve is on a
comparable scale to the base fee, ignore the
reserve. Only check the account balance.
*/
auto const reserve = view.fees().accountReserve(0);
if (totalFee >= balance || totalFee >= reserve)
auto const base = view.fees().base;
if (totalFee >= balance ||
(reserve > 10 * base && totalFee >= reserve))
{
// Drop the current transaction
JLOG(j_.trace()) << "Ignoring transaction " << transactionID
@@ -1104,7 +1116,10 @@ TxQ::apply(
// inserted in the middle from fouling up later transactions.
auto const potentialTotalSpend = totalFee +
std::min(balance - std::min(balance, reserve), potentialSpend);
assert(potentialTotalSpend > XRPAmount{0});
assert(
potentialTotalSpend > XRPAmount{0} ||
(potentialTotalSpend == XRPAmount{0} &&
multiTxn->applyView.fees().base == 0));
sleBump->setFieldAmount(sfBalance, balance - potentialTotalSpend);
// The transaction's sequence/ticket will be valid when the other
// transactions in the queue have been processed. If the tx has a
@@ -1758,7 +1773,7 @@ TxQ::getTxRequiredFeeAndSeq(
std::lock_guard lock(mutex_);
auto const snapshot = feeMetrics_.getSnapshot();
auto const baseFee = view.fees().toDrops(calculateBaseFee(view, *tx));
auto const baseFee = calculateBaseFee(view, *tx);
auto const fee = FeeMetrics::scaleFeeLevel(snapshot, view);
auto const sle = view.read(keylet::account(account));
@@ -1834,15 +1849,26 @@ TxQ::doRPC(Application& app) const
levels[jss::open_ledger_level] = to_string(metrics.openLedgerFeeLevel);
auto const baseFee = view->fees().base;
// If the base fee is 0 drops, but escalation has kicked in, treat the
// base fee as if it is 1 drop, which makes the rest of the math
// work.
auto const effectiveBaseFee = [&baseFee, &metrics]() {
if (!baseFee && metrics.openLedgerFeeLevel != metrics.referenceFeeLevel)
return XRPAmount{1};
return baseFee;
}();
auto& drops = ret[jss::drops] = Json::Value();
drops[jss::base_fee] =
to_string(toDrops(metrics.referenceFeeLevel, baseFee));
drops[jss::minimum_fee] =
to_string(toDrops(metrics.minProcessingFeeLevel, baseFee));
drops[jss::base_fee] = to_string(baseFee);
drops[jss::median_fee] = to_string(toDrops(metrics.medFeeLevel, baseFee));
drops[jss::open_ledger_fee] = to_string(
toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1);
drops[jss::minimum_fee] = to_string(toDrops(
metrics.minProcessingFeeLevel,
metrics.txCount >= metrics.txQMaxSize ? effectiveBaseFee : baseFee));
auto openFee = toDrops(metrics.openLedgerFeeLevel, effectiveBaseFee);
if (effectiveBaseFee &&
toFeeLevel(openFee, effectiveBaseFee) < metrics.openLedgerFeeLevel)
openFee += 1;
drops[jss::open_ledger_fee] = to_string(openFee);
return ret;
}

View File

@@ -300,7 +300,7 @@ preclaim(
@return The base fee.
*/
FeeUnit64
XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Return the minimum fee that an "ordinary" transaction would pay.

View File

@@ -33,7 +33,7 @@ ApplyContext::ApplyContext(
OpenView& base,
STTx const& tx_,
TER preclaimResult_,
FeeUnit64 baseFee_,
XRPAmount baseFee_,
ApplyFlags flags,
beast::Journal journal_)
: app(app_)

View File

@@ -40,14 +40,14 @@ public:
OpenView& base,
STTx const& tx,
TER preclaimResult,
FeeUnit64 baseFee,
XRPAmount baseFee,
ApplyFlags flags,
beast::Journal = beast::Journal{beast::Journal::getNullSink()});
Application& app;
STTx const& tx;
TER const preclaimResult;
FeeUnit64 const baseFee;
XRPAmount const baseFee;
beast::Journal const journal;
ApplyView&

View File

@@ -90,8 +90,46 @@ Change::preclaim(PreclaimContext const& ctx)
switch (ctx.tx.getTxnType())
{
case ttAMENDMENT:
case ttFEE:
if (ctx.view.rules().enabled(featureXRPFees))
{
// The ttFEE transaction format defines these fields as
// optional, but once the XRPFees feature is enabled, they are
// required.
if (!ctx.tx.isFieldPresent(sfBaseFeeDrops) ||
!ctx.tx.isFieldPresent(sfReserveBaseDrops) ||
!ctx.tx.isFieldPresent(sfReserveIncrementDrops))
return temMALFORMED;
// The ttFEE transaction format defines these fields as
// optional, but once the XRPFees feature is enabled, they are
// forbidden.
if (ctx.tx.isFieldPresent(sfBaseFee) ||
ctx.tx.isFieldPresent(sfReferenceFeeUnits) ||
ctx.tx.isFieldPresent(sfReserveBase) ||
ctx.tx.isFieldPresent(sfReserveIncrement))
return temMALFORMED;
}
else
{
// The ttFEE transaction format formerly defined these fields
// as required. When the XRPFees feature was implemented, they
// were changed to be optional. Until the feature has been
// enabled, they are required.
if (!ctx.tx.isFieldPresent(sfBaseFee) ||
!ctx.tx.isFieldPresent(sfReferenceFeeUnits) ||
!ctx.tx.isFieldPresent(sfReserveBase) ||
!ctx.tx.isFieldPresent(sfReserveIncrement))
return temMALFORMED;
// The ttFEE transaction format defines these fields as
// optional, but without the XRPFees feature, they are
// forbidden.
if (ctx.tx.isFieldPresent(sfBaseFeeDrops) ||
ctx.tx.isFieldPresent(sfReserveBaseDrops) ||
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
return temDISABLED;
}
return tesSUCCESS;
case ttAMENDMENT:
case ttUNL_MODIFY:
return tesSUCCESS;
default:
@@ -315,13 +353,27 @@ Change::applyFee()
feeObject = std::make_shared<SLE>(k);
view().insert(feeObject);
}
feeObject->setFieldU64(sfBaseFee, ctx_.tx.getFieldU64(sfBaseFee));
feeObject->setFieldU32(
sfReferenceFeeUnits, ctx_.tx.getFieldU32(sfReferenceFeeUnits));
feeObject->setFieldU32(sfReserveBase, ctx_.tx.getFieldU32(sfReserveBase));
feeObject->setFieldU32(
sfReserveIncrement, ctx_.tx.getFieldU32(sfReserveIncrement));
auto set = [](SLE::pointer& feeObject, STTx const& tx, auto const& field) {
feeObject->at(field) = tx[field];
};
if (view().rules().enabled(featureXRPFees))
{
set(feeObject, ctx_.tx, sfBaseFeeDrops);
set(feeObject, ctx_.tx, sfReserveBaseDrops);
set(feeObject, ctx_.tx, sfReserveIncrementDrops);
// Ensure the old fields are removed
feeObject->makeFieldAbsent(sfBaseFee);
feeObject->makeFieldAbsent(sfReferenceFeeUnits);
feeObject->makeFieldAbsent(sfReserveBase);
feeObject->makeFieldAbsent(sfReserveIncrement);
}
else
{
set(feeObject, ctx_.tx, sfBaseFee);
set(feeObject, ctx_.tx, sfReferenceFeeUnits);
set(feeObject, ctx_.tx, sfReserveBase);
set(feeObject, ctx_.tx, sfReserveIncrement);
}
view().update(feeObject);

View File

@@ -46,10 +46,10 @@ public:
void
preCompute() override;
static FeeUnit64
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx)
{
return FeeUnit64{0};
return XRPAmount{0};
}
static TER

View File

@@ -52,20 +52,11 @@ DeleteAccount::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
FeeUnit64
XRPAmount
DeleteAccount::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// The fee required for AccountDelete is one owner reserve. But the
// owner reserve is stored in drops. We need to convert it to fee units.
Fees const& fees{view.fees()};
std::pair<bool, FeeUnit64> const mulDivResult{
mulDiv(fees.increment, safe_cast<FeeUnit64>(fees.units), fees.base)};
if (mulDivResult.first)
return mulDivResult.second;
// If mulDiv returns false then overflow happened. Punt by using the
// standard calculation.
return Transactor::calculateBaseFee(view, tx);
// The fee required for AccountDelete is one owner reserve.
return view.fees().increment;
}
namespace {

View File

@@ -38,7 +38,7 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static FeeUnit64
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER

View File

@@ -338,15 +338,14 @@ EscrowFinish::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
FeeUnit64
XRPAmount
EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
{
FeeUnit64 extraFee{0};
XRPAmount extraFee{0};
if (auto const fb = tx[~sfFulfillment])
{
extraFee +=
safe_cast<FeeUnit64>(view.fees().units) * (32 + (fb->size() / 16));
extraFee += view.fees().base * (32 + (fb->size() / 16));
}
return Transactor::calculateBaseFee(view, tx) + extraFee;

View File

@@ -57,7 +57,7 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static FeeUnit64
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
TER

View File

@@ -24,7 +24,7 @@
namespace ripple {
FeeUnit64
XRPAmount
SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const id = tx.getAccountID(sfAccount);
@@ -39,7 +39,7 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx)
if (sle && (!(sle->getFlags() & lsfPasswordSpent)))
{
// flag is armed and they signed with the right account
return FeeUnit64{0};
return XRPAmount{0};
}
}
}

View File

@@ -39,7 +39,7 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static FeeUnit64
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
TER

View File

@@ -137,7 +137,7 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
FeeUnit64
XRPAmount
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// Returns the fee in fee units.
@@ -145,7 +145,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
// The computation has two parts:
// * The base fee, which is the same for most transactions.
// * The additional cost of each multisignature on the transaction.
FeeUnit64 const baseFee = safe_cast<FeeUnit64>(view.fees().units);
XRPAmount const baseFee = view.fees().base;
// Each signer adds one more baseFee to the minimum required fee
// for the transaction.
@@ -158,7 +158,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
XRPAmount
Transactor::minimumFee(
Application& app,
FeeUnit64 baseFee,
XRPAmount baseFee,
Fees const& fees,
ApplyFlags flags)
{
@@ -166,7 +166,7 @@ Transactor::minimumFee(
}
TER
Transactor::checkFee(PreclaimContext const& ctx, FeeUnit64 baseFee)
Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
if (!ctx.tx[sfFee].native())
return temBAD_FEE;

View File

@@ -132,13 +132,13 @@ public:
checkPriorTxAndLastLedger(PreclaimContext const& ctx);
static TER
checkFee(PreclaimContext const& ctx, FeeUnit64 baseFee);
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
static NotTEC
checkSign(PreclaimContext const& ctx);
// Returns the fee in fee units, not scaled for load.
static FeeUnit64
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
@@ -182,7 +182,7 @@ protected:
static XRPAmount
minimumFee(
Application& app,
FeeUnit64 baseFee,
XRPAmount baseFee,
Fees const& fees,
ApplyFlags flags);

View File

@@ -254,7 +254,7 @@ invoke_preclaim(PreclaimContext const& ctx)
}
}
static FeeUnit64
static XRPAmount
invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
{
switch (tx.getTxnType())
@@ -313,7 +313,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
return NFTokenAcceptOffer::calculateBaseFee(view, tx);
default:
assert(false);
return FeeUnit64{0};
return XRPAmount{0};
}
}
@@ -535,7 +535,7 @@ preclaim(
}
}
FeeUnit64
XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx)
{
return invoke_calculateBaseFee(view, tx);
@@ -544,7 +544,7 @@ calculateBaseFee(ReadView const& view, STTx const& tx)
XRPAmount
calculateDefaultBaseFee(ReadView const& view, STTx const& tx)
{
return view.fees().toDrops(Transactor::calculateBaseFee(view, tx));
return Transactor::calculateBaseFee(view, tx);
}
std::pair<TER, bool>

View File

@@ -454,11 +454,6 @@ mulDivU(Source1 value, Dest mul, Source2 div)
} // namespace feeunit
template <class T>
using FeeUnit = feeunit::TaggedFee<feeunit::feeunitTag, T>;
using FeeUnit32 = FeeUnit<std::uint32_t>;
using FeeUnit64 = FeeUnit<std::uint64_t>;
template <class T>
using FeeLevel = feeunit::TaggedFee<feeunit::feelevelTag, T>;
using FeeLevel64 = FeeLevel<std::uint64_t>;

View File

@@ -139,8 +139,9 @@ public:
// Network parameters
// The number of fee units a reference transaction costs
static constexpr FeeUnit32 TRANSACTION_FEE_BASE{10};
// DEPRECATED - Fee units for a reference transction.
// Only provided for backwards compatibility in a couple of places
static constexpr std::uint32_t FEE_UNITS_DEPRECATED = 10;
// Note: The following parameters do not relate to the UNL or trust at all
// Minimum number of nodes to consider the network present

View File

@@ -49,7 +49,6 @@ namespace ripple {
struct Fees
{
XRPAmount base{0}; // Reference tx cost (drops)
FeeUnit32 units{0}; // Reference fee units
XRPAmount reserve{0}; // Reserve base (drops)
XRPAmount increment{0}; // Reserve increment (drops)
@@ -68,15 +67,6 @@ struct Fees
{
return reserve + ownerCount * increment;
}
XRPAmount
toDrops(FeeUnit64 const& fee) const
{
if (auto const resultPair = mulDiv(base, fee, units); resultPair.first)
return resultPair.second;
return XRPAmount(STAmount::cMaxNativeN);
}
};
//------------------------------------------------------------------------------

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 = 54;
static constexpr std::size_t numFeatures = 55;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -341,6 +341,7 @@ extern uint256 const fixTrustLinesToSelf;
extern uint256 const fixRemoveNFTokenAutoTrustLine;
extern uint256 const featureImmediateOfferKilled;
extern uint256 const featureDisallowIncoming;
extern uint256 const featureXRPFees;
} // namespace ripple

View File

@@ -483,6 +483,11 @@ extern SF_AMOUNT const sfRippleEscrow;
extern SF_AMOUNT const sfDeliveredAmount;
extern SF_AMOUNT const sfNFTokenBrokerFee;
// currency amount (fees)
extern SF_AMOUNT const sfBaseFeeDrops;
extern SF_AMOUNT const sfReserveBaseDrops;
extern SF_AMOUNT const sfReserveIncrementDrops;
// variable length (common)
extern SF_VL const sfPublicKey;
extern SF_VL const sfMessageKey;

View File

@@ -49,6 +49,14 @@ isLegalAmount(XRPAmount const& amount)
return amount <= INITIAL_XRP;
}
/** Returns true if the absolute value of the amount does not exceed the initial
* XRP in existence. */
inline bool
isLegalAmountSigned(XRPAmount const& amount)
{
return amount >= -INITIAL_XRP && amount <= INITIAL_XRP;
}
/* The currency code for the native currency. */
static inline std::string const&
systemCurrencyCode()

View File

@@ -451,6 +451,7 @@ REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no)
REGISTER_FIX (fixRemoveNFTokenAutoTrustLine, Supported::yes, DefaultVote::yes);
REGISTER_FEATURE(ImmediateOfferKilled, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no);
// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -146,10 +146,15 @@ LedgerFormats::LedgerFormats()
add(jss::FeeSettings,
ltFEE_SETTINGS,
{
{sfBaseFee, soeREQUIRED},
{sfReferenceFeeUnits, soeREQUIRED},
{sfReserveBase, soeREQUIRED},
{sfReserveIncrement, soeREQUIRED},
// Old version uses raw numbers
{sfBaseFee, soeOPTIONAL},
{sfReferenceFeeUnits, soeOPTIONAL},
{sfReserveBase, soeOPTIONAL},
{sfReserveIncrement, soeOPTIONAL},
// New version uses Amounts
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
},
commonFields);

View File

@@ -234,6 +234,13 @@ CONSTRUCT_TYPED_SFIELD(sfRippleEscrow, "RippleEscrow", AMOUNT,
CONSTRUCT_TYPED_SFIELD(sfDeliveredAmount, "DeliveredAmount", AMOUNT, 18);
CONSTRUCT_TYPED_SFIELD(sfNFTokenBrokerFee, "NFTokenBrokerFee", AMOUNT, 19);
// Reserve 20 & 21 for Hooks
// currency amount (fees)
CONSTRUCT_TYPED_SFIELD(sfBaseFeeDrops, "BaseFeeDrops", AMOUNT, 22);
CONSTRUCT_TYPED_SFIELD(sfReserveBaseDrops, "ReserveBaseDrops", AMOUNT, 23);
CONSTRUCT_TYPED_SFIELD(sfReserveIncrementDrops, "ReserveIncrementDrops", AMOUNT, 24);
// variable length (common)
CONSTRUCT_TYPED_SFIELD(sfPublicKey, "PublicKey", VL, 1);
CONSTRUCT_TYPED_SFIELD(sfMessageKey, "MessageKey", VL, 2);

View File

@@ -58,9 +58,14 @@ STValidation::validationFormat()
{sfSigningPubKey, soeREQUIRED},
{sfSignature, soeREQUIRED},
{sfConsensusHash, soeOPTIONAL},
// featureHardenedValidations
{sfCookie, soeDEFAULT},
{sfValidatedHash, soeOPTIONAL},
{sfServerVersion, soeOPTIONAL},
// featureXRPFees
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
};
// clang-format on

View File

@@ -155,10 +155,15 @@ TxFormats::TxFormats()
ttFEE,
{
{sfLedgerSequence, soeOPTIONAL},
{sfBaseFee, soeREQUIRED},
{sfReferenceFeeUnits, soeREQUIRED},
{sfReserveBase, soeREQUIRED},
{sfReserveIncrement, soeREQUIRED},
// Old version uses raw numbers
{sfBaseFee, soeOPTIONAL},
{sfReferenceFeeUnits, soeOPTIONAL},
{sfReserveBase, soeOPTIONAL},
{sfReserveIncrement, soeOPTIONAL},
// New version uses Amounts
{sfBaseFeeDrops, soeOPTIONAL},
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
},
commonFields);

View File

@@ -151,6 +151,7 @@ JSS(balance); // out: AccountLines
JSS(balances); // out: GatewayBalances
JSS(base); // out: LogLevel
JSS(base_fee); // out: NetworkOPs
JSS(base_fee_drops); // out: NetworkOPs
JSS(base_fee_xrp); // out: NetworkOPs
JSS(bids); // out: Subscribe
JSS(binary); // in: AccountTX, LedgerEntry,
@@ -495,8 +496,10 @@ JSS(request); // RPC
JSS(requested); // out: Manifest
JSS(reservations); // out: Reservations
JSS(reserve_base); // out: NetworkOPs
JSS(reserve_base_drops); // out: NetworkOPs
JSS(reserve_base_xrp); // out: NetworkOPs
JSS(reserve_inc); // out: NetworkOPs
JSS(reserve_inc_drops); // out: NetworkOPs
JSS(reserve_inc_xrp); // out: NetworkOPs
JSS(response); // websocket
JSS(result); // RPC

View File

@@ -45,7 +45,7 @@ fillTransaction(
// Convert the reference transaction cost in fee units to drops
// scaled to represent the current fee load.
txArray["Fee"] =
scaleFeeLoad(fees.units, context.app.getFeeTrack(), fees, false)
scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false)
.jsonClipped();
}

View File

@@ -720,8 +720,7 @@ checkFee(
}
}
// Default fee in fee units.
FeeUnit32 const feeDefault = config.TRANSACTION_FEE_BASE;
XRPAmount const feeDefault = config.FEE_DEFAULT;
auto ledger = app.openLedger().current();
// Administrative and identified endpoints are exempt from local fees.
@@ -738,11 +737,7 @@ checkFee(
auto const limit = [&]() {
// Scale fee units to drops:
auto const drops =
mulDiv(feeDefault, ledger->fees().base, ledger->fees().units);
if (!drops.first)
Throw<std::overflow_error>("mulDiv");
auto const result = mulDiv(drops.second, mult, div);
auto const result = mulDiv(feeDefault, mult, div);
if (!result.first)
Throw<std::overflow_error>("mulDiv");
return result.second;

View File

@@ -515,7 +515,10 @@ public:
// All it takes is a large enough XRP payment to resurrect
// becky's account. Try too small a payment.
env(pay(alice, becky, XRP(9)), ter(tecNO_DST_INSUF_XRP));
env(pay(alice,
becky,
drops(env.current()->fees().accountReserve(0)) - XRP(1)),
ter(tecNO_DST_INSUF_XRP));
env.close();
// Actually resurrect becky's account.

View File

@@ -29,13 +29,14 @@ class FeeVote_test : public beast::unit_test::suite
void
testSetup()
{
FeeVote::Setup const defaultSetup;
{
// defaults
Section config;
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == 10);
BEAST_EXPECT(setup.account_reserve == 10 * DROPS_PER_XRP);
BEAST_EXPECT(setup.owner_reserve == 2 * DROPS_PER_XRP);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
}
{
Section config;
@@ -56,9 +57,9 @@ class FeeVote_test : public beast::unit_test::suite
"owner_reserve = foo"});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == 10);
BEAST_EXPECT(setup.account_reserve == 10 * DROPS_PER_XRP);
BEAST_EXPECT(setup.owner_reserve == 2 * DROPS_PER_XRP);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
}
{
Section config;
@@ -68,7 +69,7 @@ class FeeVote_test : public beast::unit_test::suite
"owner_reserve = -1234"});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == 10);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(
setup.account_reserve == static_cast<std::uint32_t>(-1234567));
BEAST_EXPECT(
@@ -86,9 +87,9 @@ class FeeVote_test : public beast::unit_test::suite
"owner_reserve = " + big64});
// Illegal values are ignored, and the defaults left unchanged
auto setup = setup_FeeVote(config);
BEAST_EXPECT(setup.reference_fee == 10);
BEAST_EXPECT(setup.account_reserve == 10 * DROPS_PER_XRP);
BEAST_EXPECT(setup.owner_reserve == 2 * DROPS_PER_XRP);
BEAST_EXPECT(setup.reference_fee == defaultSetup.reference_fee);
BEAST_EXPECT(setup.account_reserve == defaultSetup.account_reserve);
BEAST_EXPECT(setup.owner_reserve == defaultSetup.owner_reserve);
}
}

View File

@@ -36,55 +36,52 @@ public:
Fees const fees = [&]() {
Fees f;
f.base = d.FEE_DEFAULT;
f.units = d.TRANSACTION_FEE_BASE;
f.reserve = 200 * DROPS_PER_XRP;
f.increment = 50 * DROPS_PER_XRP;
return f;
}();
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{0}, l, fees, false) == XRPAmount{0});
scaleFeeLoad(XRPAmount{0}, l, fees, false) == XRPAmount{0});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{10000}, l, fees, false) ==
scaleFeeLoad(XRPAmount{10000}, l, fees, false) ==
XRPAmount{10000});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{1}, l, fees, false) == XRPAmount{1});
scaleFeeLoad(XRPAmount{1}, l, fees, false) == XRPAmount{1});
}
{
Fees const fees = [&]() {
Fees f;
f.base = d.FEE_DEFAULT * 10;
f.units = d.TRANSACTION_FEE_BASE;
f.reserve = 200 * DROPS_PER_XRP;
f.increment = 50 * DROPS_PER_XRP;
return f;
}();
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{0}, l, fees, false) == XRPAmount{0});
scaleFeeLoad(XRPAmount{0}, l, fees, false) == XRPAmount{0});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{10000}, l, fees, false) ==
XRPAmount{100000});
scaleFeeLoad(XRPAmount{10000}, l, fees, false) ==
XRPAmount{10000});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{1}, l, fees, false) == XRPAmount{10});
scaleFeeLoad(XRPAmount{1}, l, fees, false) == XRPAmount{1});
}
{
Fees const fees = [&]() {
Fees f;
f.base = d.FEE_DEFAULT;
f.units = d.TRANSACTION_FEE_BASE * 10;
f.reserve = 200 * DROPS_PER_XRP;
f.increment = 50 * DROPS_PER_XRP;
return f;
}();
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{0}, l, fees, false) == XRPAmount{0});
scaleFeeLoad(XRPAmount{0}, l, fees, false) == XRPAmount{0});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{10000}, l, fees, false) ==
XRPAmount{1000});
scaleFeeLoad(XRPAmount{10000}, l, fees, false) ==
XRPAmount{10000});
BEAST_EXPECT(
scaleFeeLoad(FeeUnit64{1}, l, fees, false) == XRPAmount{0});
scaleFeeLoad(XRPAmount{1}, l, fees, false) == XRPAmount{1});
}
}
};

View File

@@ -16,6 +16,7 @@
//==============================================================================
#include <ripple/app/tx/apply.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/STAccount.h>
#include <string>
#include <test/jtx.h>
@@ -27,17 +28,26 @@ namespace test {
struct PseudoTx_test : public beast::unit_test::suite
{
std::vector<STTx>
getPseudoTxs(std::uint32_t seq)
getPseudoTxs(Rules const& rules, std::uint32_t seq)
{
std::vector<STTx> res;
res.emplace_back(STTx(ttFEE, [&](auto& obj) {
obj[sfAccount] = AccountID();
obj[sfLedgerSequence] = seq;
if (rules.enabled(featureXRPFees))
{
obj[sfBaseFeeDrops] = XRPAmount{0};
obj[sfReserveBaseDrops] = XRPAmount{0};
obj[sfReserveIncrementDrops] = XRPAmount{0};
}
else
{
obj[sfBaseFee] = 0;
obj[sfReserveBase] = 0;
obj[sfReserveIncrement] = 0;
obj[sfReferenceFeeUnits] = 0;
}
}));
res.emplace_back(STTx(ttAMENDMENT, [&](auto& obj) {
@@ -66,12 +76,13 @@ struct PseudoTx_test : public beast::unit_test::suite
}
void
testPrevented()
testPrevented(FeatureBitset features)
{
using namespace jtx;
Env env(*this);
Env env(*this, features);
for (auto const& stx : getPseudoTxs(env.closed()->seq() + 1))
for (auto const& stx :
getPseudoTxs(env.closed()->rules(), env.closed()->seq() + 1))
{
std::string reason;
BEAST_EXPECT(isPseudoTx(stx));
@@ -101,7 +112,12 @@ struct PseudoTx_test : public beast::unit_test::suite
void
run() override
{
testPrevented();
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
FeatureBitset const xrpFees{featureXRPFees};
testPrevented(all - featureXRPFees);
testPrevented(all);
testAllowed();
}
};

View File

@@ -144,9 +144,15 @@ class TxQ1_test : public beast::unit_test::suite
auto const& view = *env.current();
auto metrics = env.app().getTxQ().getMetrics(view);
auto const base = [&view]() {
auto base = view.fees().base;
if (!base)
base += 1;
return base;
}();
// Don't care about the overflow flag
return fee(toDrops(metrics.openLedgerFeeLevel, view.fees().base) + 1);
return fee(toDrops(metrics.openLedgerFeeLevel, base) + 1);
}
static std::unique_ptr<Config>
@@ -189,7 +195,6 @@ class TxQ1_test : public beast::unit_test::suite
std::size_t expectedPerLedger,
std::size_t ledgersInQueue,
std::uint32_t base,
std::uint32_t units,
std::uint32_t reserve,
std::uint32_t increment)
{
@@ -219,7 +224,6 @@ class TxQ1_test : public beast::unit_test::suite
checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, expectedPerLedger, 256);
auto const fees = env.current()->fees();
BEAST_EXPECT(fees.base == XRPAmount{base});
BEAST_EXPECT(fees.units == FeeUnit64{units});
BEAST_EXPECT(fees.reserve == XRPAmount{reserve});
BEAST_EXPECT(fees.increment == XRPAmount{increment});
@@ -1095,7 +1099,7 @@ public:
checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3, 256);
// ledgers in queue is 2 because of makeConfig
auto const initQueueMax = initFee(env, 3, 2, 10, 10, 200, 50);
auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50);
// Create several accounts while the fee is cheap so they all apply.
env.fund(drops(2000), noripple(alice));
@@ -1742,7 +1746,7 @@ public:
auto queued = ter(terQUEUED);
// ledgers in queue is 2 because of makeConfig
auto const initQueueMax = initFee(env, 3, 2, 10, 10, 200, 50);
auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50);
BEAST_EXPECT(env.current()->fees().base == 10);
@@ -2137,7 +2141,7 @@ public:
// queued before the open ledger fee approached the reserve,
// which would unnecessarily slow down this test.
// ledgers in queue is 2 because of makeConfig
auto const initQueueMax = initFee(env, 3, 2, 10, 10, 200, 50);
auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50);
auto limit = 3;
@@ -4785,6 +4789,144 @@ public:
}
}
void
testZeroReferenceFee()
{
testcase("Zero reference fee");
using namespace jtx;
Account const alice("alice");
auto const queued = ter(terQUEUED);
Env env(
*this,
makeConfig(
{{"minimum_txn_in_ledger_standalone", "3"}},
{{"reference_fee", "0"},
{"account_reserve", "0"},
{"owner_reserve", "0"}}));
BEAST_EXPECT(env.current()->fees().base == 10);
checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3, 256);
// ledgers in queue is 2 because of makeConfig
auto const initQueueMax = initFee(env, 3, 2, 0, 0, 0);
BEAST_EXPECT(env.current()->fees().base == 0);
{
auto const fee = env.rpc("fee");
if (BEAST_EXPECT(fee.isMember(jss::result)) &&
BEAST_EXPECT(!RPC::contains_error(fee[jss::result])))
{
auto const& result = fee[jss::result];
BEAST_EXPECT(result.isMember(jss::levels));
auto const& levels = result[jss::levels];
BEAST_EXPECT(
levels.isMember(jss::median_level) &&
levels[jss::median_level] == "128000");
BEAST_EXPECT(
levels.isMember(jss::minimum_level) &&
levels[jss::minimum_level] == "256");
BEAST_EXPECT(
levels.isMember(jss::open_ledger_level) &&
levels[jss::open_ledger_level] == "256");
BEAST_EXPECT(
levels.isMember(jss::reference_level) &&
levels[jss::reference_level] == "256");
auto const& drops = result[jss::drops];
BEAST_EXPECT(
drops.isMember(jss::base_fee) &&
drops[jss::base_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::median_fee) &&
drops[jss::base_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::minimum_fee) &&
drops[jss::base_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::open_ledger_fee) &&
drops[jss::base_fee] == "0");
}
}
checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3, 256);
// The noripple is to reduce the number of transactions required to
// fund the accounts. There is no rippling in this test.
env.fund(XRP(100000), noripple(alice));
checkMetrics(__LINE__, env, 0, initQueueMax, 1, 3, 256);
env.close();
checkMetrics(__LINE__, env, 0, 6, 0, 3, 256);
fillQueue(env, alice);
checkMetrics(__LINE__, env, 0, 6, 4, 3, 256);
env(noop(alice), openLedgerFee(env));
checkMetrics(__LINE__, env, 0, 6, 5, 3, 256);
auto aliceSeq = env.seq(alice);
env(noop(alice), queued);
checkMetrics(__LINE__, env, 1, 6, 5, 3, 256);
env(noop(alice), seq(aliceSeq + 1), fee(10), queued);
checkMetrics(__LINE__, env, 2, 6, 5, 3, 256);
{
auto const fee = env.rpc("fee");
if (BEAST_EXPECT(fee.isMember(jss::result)) &&
BEAST_EXPECT(!RPC::contains_error(fee[jss::result])))
{
auto const& result = fee[jss::result];
BEAST_EXPECT(result.isMember(jss::levels));
auto const& levels = result[jss::levels];
BEAST_EXPECT(
levels.isMember(jss::median_level) &&
levels[jss::median_level] == "128000");
BEAST_EXPECT(
levels.isMember(jss::minimum_level) &&
levels[jss::minimum_level] == "256");
BEAST_EXPECT(
levels.isMember(jss::open_ledger_level) &&
levels[jss::open_ledger_level] == "355555");
BEAST_EXPECT(
levels.isMember(jss::reference_level) &&
levels[jss::reference_level] == "256");
auto const& drops = result[jss::drops];
BEAST_EXPECT(
drops.isMember(jss::base_fee) &&
drops[jss::base_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::median_fee) &&
drops[jss::median_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::minimum_fee) &&
drops[jss::minimum_fee] == "0");
BEAST_EXPECT(
drops.isMember(jss::open_ledger_fee) &&
drops[jss::open_ledger_fee] == "1389");
}
}
env.close();
checkMetrics(__LINE__, env, 0, 10, 2, 5, 256);
}
void
run() override
{
@@ -4825,6 +4967,7 @@ public:
testReexecutePreflight();
testQueueFullDropPenalty();
testCancelQueuedOffers();
testZeroReferenceFee();
}
};

View File

@@ -30,6 +30,8 @@ private:
void
testTypes()
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount x{100};
BEAST_EXPECT(x.drops() == 100);
@@ -45,8 +47,8 @@ private:
BEAST_EXPECT(
(std::is_same_v<decltype(z)::unit_type, feeunit::dropTag>));
FeeUnit32 f{10};
FeeUnit32 baseFee{100};
FeeLevel32 f{10};
FeeLevel32 baseFee{100};
auto drops = mulDiv(baseFee, x, f).second;
@@ -65,8 +67,8 @@ private:
BEAST_EXPECT(
(std::is_same_v<decltype(y)::unit_type, feeunit::dropTag>));
FeeUnit64 f{10};
FeeUnit64 baseFee{100};
FeeLevel64 f{10};
FeeLevel64 baseFee{100};
auto drops = mulDiv(baseFee, x, f).second;
@@ -102,22 +104,24 @@ private:
testJson()
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeUnit32 x{std::numeric_limits<std::uint32_t>::max()};
FeeLevel32 x{std::numeric_limits<std::uint32_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == Json::uintValue);
BEAST_EXPECT(y == Json::Value{x.fee()});
}
{
FeeUnit32 x{std::numeric_limits<std::uint32_t>::min()};
FeeLevel32 x{std::numeric_limits<std::uint32_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == Json::uintValue);
BEAST_EXPECT(y == Json::Value{x.fee()});
}
{
FeeUnit64 x{std::numeric_limits<std::uint64_t>::max()};
FeeLevel64 x{std::numeric_limits<std::uint64_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == Json::uintValue);
BEAST_EXPECT(
@@ -125,7 +129,7 @@ private:
}
{
FeeUnit64 x{std::numeric_limits<std::uint64_t>::min()};
FeeLevel64 x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == Json::uintValue);
BEAST_EXPECT(y == Json::Value{0});
@@ -167,15 +171,17 @@ private:
{
// Explicitly test every defined function for the TaggedFee class
// since some of them are templated, but not used anywhere else.
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
auto make = [&](auto x) -> FeeUnit64 { return x; };
auto explicitmake = [&](auto x) -> FeeUnit64 {
return FeeUnit64{x};
auto make = [&](auto x) -> FeeLevel64 { return x; };
auto explicitmake = [&](auto x) -> FeeLevel64 {
return FeeLevel64{x};
};
FeeUnit64 defaulted;
FeeLevel64 defaulted;
(void)defaulted;
FeeUnit64 test{0};
FeeLevel64 test{0};
BEAST_EXPECT(test.fee() == 0);
test = explicitmake(beast::zero);
@@ -187,13 +193,13 @@ private:
test = explicitmake(100u);
BEAST_EXPECT(test.fee() == 100);
FeeUnit64 const targetSame{200u};
FeeUnit32 const targetOther{300u};
FeeLevel64 const targetSame{200u};
FeeLevel32 const targetOther{300u};
test = make(targetSame);
BEAST_EXPECT(test.fee() == 200);
BEAST_EXPECT(test == targetSame);
BEAST_EXPECT(test < FeeUnit64{1000});
BEAST_EXPECT(test > FeeUnit64{100});
BEAST_EXPECT(test < FeeLevel64{1000});
BEAST_EXPECT(test > FeeLevel64{100});
test = make(targetOther);
BEAST_EXPECT(test.fee() == 300);
BEAST_EXPECT(test == targetOther);

View File

@@ -64,7 +64,7 @@ class Invariants_test : public beast::unit_test::suite
ov,
tx,
tesSUCCESS,
safe_cast<FeeUnit64>(env.current()->fees().units),
env.current()->fees().base,
tapNONE,
jlog};

View File

@@ -547,7 +547,10 @@ class AccountTx_test : public beast::unit_test::suite
// All it takes is a large enough XRP payment to resurrect
// becky's account. Try too small a payment.
env(pay(alice, becky, XRP(9)), ter(tecNO_DST_INSUF_XRP));
env(pay(alice,
becky,
drops(env.current()->fees().accountReserve(0)) - XRP(1)),
ter(tecNO_DST_INSUF_XRP));
env.close();
// Actually resurrect becky's account.

View File

@@ -20,6 +20,7 @@
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/beast/unit_test.h>
#include <ripple/core/ConfigSections.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <test/jtx/WSClient.h>
@@ -326,11 +327,11 @@ public:
}
void
testValidations()
testValidations(FeatureBitset features)
{
using namespace jtx;
Env env{*this, envconfig(validator, "")};
Env env{*this, envconfig(validator, ""), features};
auto& cfg = env.app().config();
if (!BEAST_EXPECT(cfg.section(SECTION_VALIDATION_SEED).empty()))
return;
@@ -410,10 +411,25 @@ public:
if (jv.isMember(jss::server_version) != isFlagLedger)
return false;
if (jv.isMember(jss::reserve_base) != isFlagLedger)
bool xrpFees = env.closed()->rules().enabled(featureXRPFees);
if ((!xrpFees &&
jv.isMember(jss::reserve_base) != isFlagLedger) ||
(xrpFees && jv.isMember(jss::reserve_base)))
return false;
if (jv.isMember(jss::reserve_inc) != isFlagLedger)
if ((!xrpFees &&
jv.isMember(jss::reserve_inc) != isFlagLedger) ||
(xrpFees && jv.isMember(jss::reserve_inc)))
return false;
if ((xrpFees &&
jv.isMember(jss::reserve_base_drops) != isFlagLedger) ||
(!xrpFees && jv.isMember(jss::reserve_base_drops)))
return false;
if ((xrpFees &&
jv.isMember(jss::reserve_inc_drops) != isFlagLedger) ||
(!xrpFees && jv.isMember(jss::reserve_inc_drops)))
return false;
return true;
@@ -1140,11 +1156,16 @@ public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
FeatureBitset const xrpFees{featureXRPFees};
testServer();
testLedger();
testTransactions();
testManifests();
testValidations();
testValidations(all - xrpFees);
testValidations(all);
testSubErrors(true);
testSubErrors(false);
testSubByUrl();