Skip to content

Commit

Permalink
Improve handling of 0 drop reference fee with TxQ
Browse files Browse the repository at this point in the history
* For use with CDBC and sidechain projects 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.
  • Loading branch information
ximinez committed Jul 27, 2022
1 parent 92fc9ba commit faa9a83
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/ripple/app/misc/TxQ.h
Expand Up @@ -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;
Expand Down
42 changes: 30 additions & 12 deletions src/ripple/app/misc/impl/TxQ.cpp
Expand Up @@ -41,11 +41,15 @@ getFeeLevelPaid(ReadView const& view, STTx const& 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);
Expand Down Expand Up @@ -1106,7 +1110,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
Expand Down Expand Up @@ -1836,15 +1843,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;
}
Expand Down
154 changes: 149 additions & 5 deletions src/test/app/TxQ_test.cpp
Expand Up @@ -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>
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -1094,7 +1099,7 @@ class TxQ1_test : public beast::unit_test::suite
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));
Expand Down Expand Up @@ -1741,7 +1746,7 @@ class TxQ1_test : public beast::unit_test::suite
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);

Expand Down Expand Up @@ -2136,7 +2141,7 @@ class TxQ1_test : public beast::unit_test::suite
// 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;

Expand Down Expand Up @@ -4784,6 +4789,144 @@ class TxQ1_test : public beast::unit_test::suite
}
}

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", "200"},
{"owner_reserve", "50"}}));

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, 200, 50);

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
{
Expand Down Expand Up @@ -4824,6 +4967,7 @@ class TxQ1_test : public beast::unit_test::suite
testReexecutePreflight();
testQueueFullDropPenalty();
testCancelQueuedOffers();
testZeroReferenceFee();
}
};

Expand Down

0 comments on commit faa9a83

Please sign in to comment.