Merge branch 'develop' into ximinez/online-delete-gaps

This commit is contained in:
Ed Hennis
2026-08-10 20:39:05 -04:00
committed by GitHub
14 changed files with 729 additions and 60 deletions

View File

@@ -4,6 +4,7 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -124,7 +125,12 @@ std::optional<EcPair>
makeEcPair(Slice const& buffer)
{
if (buffer.length() != 2 * kEcCiphertextComponentLength)
return std::nullopt; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::makeEcPair : callers must pre-validate ciphertext length");
return std::nullopt;
// LCOV_EXCL_STOP
}
auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length());
@@ -266,7 +272,13 @@ std::optional<Buffer>
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
{
if (pubKeySlice.size() != kEcPubKeyLength)
return std::nullopt; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::encryptCanonicalZeroAmount : callers must pre-validate public key length");
return std::nullopt;
// LCOV_EXCL_STOP
}
EcPair pair{};
secp256k1_pubkey pubKey;
@@ -274,14 +286,24 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M
secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength);
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::encryptCanonicalZeroAmount : public key read from the ledger must already be "
"valid");
return std::nullopt;
// LCOV_EXCL_STOP
}
if (auto res = generate_canonical_encrypted_zero(
secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data());
res != 1)
{
return std::nullopt; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::encryptCanonicalZeroAmount : canonical zero generation cannot fail for a "
"valid public key");
return std::nullopt;
// LCOV_EXCL_STOP
}
return serializeEcPair(pair);
@@ -301,7 +323,11 @@ verifyRevealedAmount(
issuer.publicKey.size() != kEcPubKeyLength ||
issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifyRevealedAmount : callers must pre-validate holder/issuer field lengths");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const holderP = toParticipant(holder);
@@ -313,7 +339,11 @@ verifyRevealedAmount(
if (auditor->publicKey.size() != kEcPubKeyLength ||
auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifyRevealedAmount : callers must pre-validate auditor field lengths");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auditorP = toParticipant(*auditor);
auditorPtr = &auditorP;
@@ -337,7 +367,12 @@ checkEncryptedAmountFormat(STObject const& object)
if (!object.isFieldPresent(sfHolderEncryptedAmount) ||
!object.isFieldPresent(sfIssuerEncryptedAmount))
{
return temMALFORMED; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::checkEncryptedAmountFormat : callers already enforce that these fields are "
"present");
return temMALFORMED;
// LCOV_EXCL_STOP
}
if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
@@ -366,7 +401,12 @@ TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{
if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::verifySchnorrProof : callers must pre-validate proof/public key length");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0)
return tecBAD_PROOF;
@@ -385,7 +425,12 @@ verifyClawbackProof(
if (ciphertext.size() != kEcGamalEncryptedTotalLength ||
pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifyClawbackProof : callers must pre-validate ciphertext/public "
"key/proof length");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (mpt_verify_clawback_proof(
@@ -420,7 +465,12 @@ verifySendProof(
amountCommitment.size() != kEcPedersenCommitmentLength ||
balanceCommitment.size() != kEcPedersenCommitmentLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifySendProof : callers must pre-validate proof/participant/commitment "
"lengths");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
std::vector<mpt_confidential_participant> participants;
@@ -433,12 +483,22 @@ verifySendProof(
if (auditor->publicKey.size() != kEcPubKeyLength ||
auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE("xrpl::verifySendProof : callers must pre-validate auditor field lengths");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
participants.push_back(toParticipant(*auditor));
}
if (participants.size() != recipientCount)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifySendProof : participant count must match the requested recipient "
"count");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (mpt_verify_send_proof(
proof.data(),
@@ -468,7 +528,12 @@ verifyConvertBackProof(
spendingBalance.size() != kEcGamalEncryptedTotalLength ||
balanceCommitment.size() != kEcPedersenCommitmentLength)
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifyConvertBackProof : callers must pre-validate proof/public "
"key/balance/commitment lengths");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (mpt_verify_convert_back_proof(

View File

@@ -73,6 +73,7 @@ TransfersNotFrozen::finalize(
* view.rules().enabled(fixFreezeExploit);
*/
[[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0);
return std::ranges::all_of(balanceChanges_, [&](auto const& entry) {
auto const& [issue, changes] = entry;
@@ -90,7 +91,7 @@ TransfersNotFrozen::finalize(
return !enforce;
}
return validateIssuerChanges(issuerSle, changes, tx, j, enforce);
return validateIssuerChanges(issuerSle, changes, tx, j, enforce, fixOverrideFreeze);
});
}
@@ -199,7 +200,8 @@ TransfersNotFrozen::validateIssuerChanges(
IssuerChanges const& changes,
STTx const& tx,
beast::Journal const& j,
bool enforce)
bool enforce,
bool fixOverrideFreeze)
{
if (!issuer)
{
@@ -225,7 +227,7 @@ TransfersNotFrozen::validateIssuerChanges(
{
bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze))
if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze, fixOverrideFreeze))
{
return false;
}
@@ -241,26 +243,29 @@ TransfersNotFrozen::validateFrozenState(
STTx const& tx,
beast::Journal const& j,
bool enforce,
bool globalFreeze)
bool globalFreeze,
bool fixOverrideFreeze)
{
bool const freeze =
change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
bool const frozen = globalFreeze || deepFreeze || freeze;
bool const isAMMLine = change.line->isFlag(lsfAMMNode);
if (!frozen)
{
return true;
}
// AMMClawbacks are allowed to override some freeze rules
if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze))
// Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on
// individually-frozen or deep-frozen AMM trust lines.
// Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types.
bool const isAMMLine = change.line->isFlag(lsfAMMNode);
if ((fixOverrideFreeze || !isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze))
{
JLOG(j.debug()) << "Invariant check allowing funds to be moved "
<< (change.balanceChangeSign > 0 ? "to" : "from")
<< " a frozen trustline for AMMClawback " << tx.getTransactionID();
<< " a frozen trustline for a freeze privileged transaction "
<< tx.getTransactionID();
return true;
}

View File

@@ -256,7 +256,7 @@ AMMClawback::applyGuts(Sandbox& sb)
}
if (!isTesSuccess(result))
return result; // LCOV_EXCL_LINE
return result;
if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3))
{
@@ -353,6 +353,13 @@ AMMClawback::equalWithdrawMatchingOneAmount(
auto amountRounded = getRoundedAsset(rules, amountBalance, frac, IsDeposit::No);
// The requested clawback amount is likely too small and results in
// one-sided pool withdrawal due to round off. Fail so the issuer can
// clawback a larger amount.
if (rules.enabled(fixCleanup3_4_0) &&
(amountRounded == beast::kZero || amount2Rounded == beast::kZero))
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
return AMMWithdraw::withdraw(
sb,
ammSle,

View File

@@ -1,6 +1,7 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTClawback.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
@@ -70,7 +71,14 @@ ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
// Sanity check: account must be the same as issuer
if (sleIssuance->getAccountID(sfIssuer) != account)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::preclaim : preflight already validated the "
"submitter is the issuer");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
@@ -127,7 +135,14 @@ ConfidentialMPTClawback::doApply()
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
if (!sleIssuance || !sleHolderMPToken)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::doApply : preclaim already validated these "
"objects exist");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const clawAmount = ctx_.tx[sfMPTAmount];
@@ -137,11 +152,25 @@ ConfidentialMPTClawback::doApply()
// After clawback, the balance should be encrypted zero.
auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
if (!encZeroForHolder)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail "
"for an already-valid holder public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
if (!encZeroForIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail "
"for an already-valid issuer public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Set holder's confidential balances to encrypted zero
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
@@ -154,14 +183,28 @@ ConfidentialMPTClawback::doApply()
// Sanity check: the issuance must have an auditor public key if
// auditing is enabled.
if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::doApply : the holder's auditor balance implies "
"the issuance has an auditor public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey];
auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
if (!encZeroForAuditor)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot "
"fail for an already-valid auditor public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
}

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -89,7 +90,14 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::preclaim : issuer derived from the MPT ID must "
"match the ledger's stored issuer");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
@@ -207,11 +215,25 @@ ConfidentialMPTConvert::doApply()
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the MPToken "
"exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the issuance "
"exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const amtToConvert = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
@@ -273,7 +295,14 @@ ConfidentialMPTConvert::doApply()
if (auditorEc)
{
if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::doApply : issuance-level auditing implies "
"the MPToken already carries an auditor balance");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
if (!sum)
@@ -308,7 +337,14 @@ ConfidentialMPTConvert::doApply()
(*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
if (!zeroBalance)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::doApply : canonical zero encryption cannot fail "
"for an already-valid holder public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
}
@@ -316,7 +352,12 @@ ConfidentialMPTConvert::doApply()
{
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
// exist together
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvert::doApply : confidential balance fields must be all "
"present or all absent");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
view().update(sleIssuance);

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -72,7 +73,14 @@ verifyProofs(
std::shared_ptr<SLE const> const& mptoken)
{
if (!mptoken->isFieldPresent(sfHolderEncryptionKey))
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::verifyProofs : preclaim already validated the holder encryption key is "
"present");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
auto const account = tx[sfAccount];
@@ -169,7 +177,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
// already checked in preflight, but should also check that issuer on
// the issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvertBack::preclaim : issuer derived from the MPT ID must "
"match the ledger's stored issuer");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleMptoken)
@@ -185,7 +200,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
// Sanity check: holder's MPToken must have auditor balance field if auditing
// is enabled
if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvertBack::preclaim : issuance-level auditing implies the "
"MPToken already carries an auditor balance");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// if the total circulating confidential balance is smaller than what the
// holder is trying to convert back, we know for sure this txn should
@@ -215,11 +237,25 @@ ConfidentialMPTConvertBack::doApply()
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the "
"MPToken exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the "
"issuance exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -49,7 +50,14 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTMergeInbox::preclaim : issuer derived from the MPT ID must "
"match the ledger's stored issuer");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
auto const sleMptoken =
ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
@@ -82,14 +90,26 @@ ConfidentialMPTMergeInbox::doApply()
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
if (!sleMptoken)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated the "
"MPToken exists");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// sanity check
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
{
return tecINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated these "
"fields are present");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Merge inbox into spending: spending = spending + inbox
@@ -114,7 +134,14 @@ ConfidentialMPTMergeInbox::doApply()
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
if (!zeroEncryption)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTMergeInbox::doApply : canonical zero encryption cannot fail "
"for an already-valid holder public key");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption);

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
@@ -105,7 +106,14 @@ verifySendProofs(
{
// Sanity check
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::detail::verifySendProofs : caller must pre-validate sender/destination/"
"issuance existence");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
@@ -204,7 +212,14 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
// Sanity check: issuer isn't the sender
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTSend::preclaim : issuer derived from the MPT ID must match "
"the ledger's stored issuer");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// Check sender's MPToken existence
auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
@@ -238,7 +253,12 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
(!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) ||
!sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance)))
{
return tefINTERNAL; // LCOV_EXCL_LINE
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTSend::preclaim : issuance-level auditing implies both "
"MPTokens already carry an auditor balance");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// Check lock
@@ -283,7 +303,14 @@ ConfidentialMPTSend::doApply()
auto const sleDestAcct = view().read(keylet::account(destination));
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::ConfidentialMPTSend::doApply : preclaim already validated these objects "
"exist");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Deposit preauth authorization was already verified in preclaim.
// Remove any expired credentials.
@@ -353,7 +380,13 @@ ConfidentialMPTSend::doApply()
auto rerandomizedDestEc = rerandomizeCiphertext(
destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
if (!rerandomizedDestEc)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed to rerandomize destination inbox ciphertext.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
@@ -374,7 +407,13 @@ ConfidentialMPTSend::doApply()
auto rerandomizedIssuerEc =
rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
if (!rerandomizedIssuerEc)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed to rerandomize destination issuer ciphertext.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
@@ -396,7 +435,13 @@ ConfidentialMPTSend::doApply()
auto rerandomizedAuditorEc = rerandomizeCiphertext(
*auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge);
if (!rerandomizedAuditorEc)
return tecINTERNAL; // LCOV_EXCL_LINE
{
// LCOV_EXCL_START
JLOG(ctx_.journal.error())
<< "ConfidentialMPTSend failed to rerandomize destination auditor ciphertext.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc);

View File

@@ -137,7 +137,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
AMM amm(env, gw, btc(100), usd(100));
env.close();
amm.deposit(alice, 1'000);
env.close();
// can not clawback when tfMPTCanClawback is not enabled
env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
@@ -503,6 +502,150 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
}
}
void
testAMMClawbackAmountRoundsToZero(FeatureBitset features)
{
// Ensure a clawback that rounds down to zero MPT fails with
// tecAMM_FAILED instead of silently burning the holder's LP.
testcase("test AMMClawback amount that rounds down to zero");
using namespace jtx;
Env env(*this, features);
Account const gw{"gateway"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10'000'000), gw, alice, bob);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
// The clawed asset (amountRounded) rounds to zero while its XRP
// counterpart is always large.
{
MPTTester const mptBtc(
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.pay = 1'000,
.flags = tfMPTCanClawback | kMptDexFlags});
MPT const btc = mptBtc;
AMM amm(env, alice, btc(3), XRP(333'000));
amm.deposit(bob, btc(3), XRP(333'000));
[[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances();
BEAST_EXPECT(poolBtcBefore == btc(6));
auto const issuerOABefore = mptBtc.getBalance(gw);
auto const aliceLpBefore = amm.getLPTokensBalance(alice.id());
auto const bobLpBefore = amm.getLPTokensBalance(bob.id());
// Attempt to clawback 1/6th of the BTC pool. When the zero-rounding
// guard is active (gated by fixCleanup3_4_0) the rounded amount
// drops to 0 and should trigger tecAMM_FAILED.
env(amm::ammClawback(gw, alice, btc, XRP, btc(1)),
Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
env.close();
[[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances();
auto const issuerOAAfter = mptBtc.getBalance(gw);
auto const aliceLpAfter = amm.getLPTokensBalance(alice.id());
auto const bobLpAfter = amm.getLPTokensBalance(bob.id());
if (features[fixCleanup3_4_0])
{
// Post-fixCleanup3_4_0: Clawback fails because the BTC balance
// would round to zero. All balances must remain untouched.
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
BEAST_EXPECT(poolXrpAfter == poolXrpBefore);
BEAST_EXPECT(issuerOAAfter == issuerOABefore);
BEAST_EXPECT(aliceLpAfter == aliceLpBefore);
BEAST_EXPECT(bobLpAfter == bobLpBefore);
}
else
{
// Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback
// silently burns alice's LP without clawing back any BTC.
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
BEAST_EXPECT(poolXrpAfter < poolXrpBefore);
BEAST_EXPECT(issuerOAAfter == issuerOABefore);
BEAST_EXPECT(aliceLpAfter < aliceLpBefore);
BEAST_EXPECT(bobLpAfter == bobLpBefore);
}
}
// The pool above only ever rounds the clawed asset (amountRounded) to
// zero; its XRP counterpart is always large. Exercise the other operand
// of the guard (amount2Rounded == 0) with an MPT/MPT pool where the
// *paired* asset is the tiny integer that floors to zero while the
// clawed asset still rounds non-zero.
{
Account const carol{"carol"};
Account const dan{"dan"};
env.fund(XRP(10'000'000), carol, dan);
env.close();
MPTTester const mptBtc(
{.env = env,
.issuer = gw,
.holders = {carol, dan},
.pay = 100'000,
.flags = tfMPTCanClawback | kMptDexFlags});
MPT const btc = mptBtc;
MPTTester const mptEth(
{.env = env,
.issuer = gw,
.holders = {carol, dan},
.pay = 1'000,
.flags = tfMPTCanClawback | kMptDexFlags});
MPT const eth = mptEth;
// btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a
// non-zero btc amount while the eth counterpart rounds to zero.
AMM amm(env, carol, btc(3'000), eth(3));
amm.deposit(dan, btc(3'000), eth(3));
[[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances();
BEAST_EXPECT(poolBtcBefore == btc(6'000));
BEAST_EXPECT(poolEthBefore == eth(6));
auto const carolLpBefore = amm.getLPTokensBalance(carol.id());
auto const danLpBefore = amm.getLPTokensBalance(dan.id());
env(amm::ammClawback(gw, carol, btc, eth, btc(500)),
Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
env.close();
[[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances();
auto const carolLpAfter = amm.getLPTokensBalance(carol.id());
auto const danLpAfter = amm.getLPTokensBalance(dan.id());
if (features[fixCleanup3_4_0])
{
// Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2)
// balance would round to zero (guard fires via
// amount2Rounded == 0). All balances must remain untouched.
BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
BEAST_EXPECT(poolEthAfter == poolEthBefore);
BEAST_EXPECT(carolLpAfter == carolLpBefore);
BEAST_EXPECT(danLpAfter == danLpBefore);
}
else
{
// Pre-fixCleanup3_4_0: the asymmetric round-off goes through.
// btc is clawed (non-zero) but eth rounds to zero, so the eth
// pool is untouched while carol's LP is burned. This asymmetry
// proves amount2Rounded == 0 is the trigger.
BEAST_EXPECT(poolBtcAfter < poolBtcBefore);
BEAST_EXPECT(poolEthAfter == poolEthBefore);
BEAST_EXPECT(carolLpAfter < carolLpBefore);
BEAST_EXPECT(danLpAfter == danLpBefore);
}
}
}
void
testAMMClawbackAll(FeatureBitset features)
{
@@ -543,7 +686,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
// gw clawback all BTC from alice
amm.deposit(bob, btc(1'000'000000), usd(2000));
env.close();
BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000)));
auto aliceBTC = env.balance(alice, btc);
@@ -921,7 +1063,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000)));
amm.deposit(bob, btc(1'000'000000), usd(4'000));
env.close();
BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000)));
auto aliceBTC = env.balance(alice, btc);
@@ -1361,7 +1502,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env.close();
BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000)));
amm.deposit(alice, btc(400));
env.close();
BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9}));
auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1407,7 +1547,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env.close();
BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
amm.deposit(alice, btc(400));
env.close();
BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1462,7 +1601,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env.close();
BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
amm.deposit(alice, btc(400));
env.close();
BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1669,7 +1807,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
// Although USD is clawable with asfAllowTrustLineClawback.
// When tfClawTwoAssets is set, we will claw Asser2 as well.
// When tfClawTwoAssets is set, we will claw Asset2 as well.
// But Asset2 is not clawable. tfMPTCanClawback was not set for BTC.
env(amm::ammClawback(gw, alice, usd, btc, std::nullopt),
Txflags(tfClawTwoAssets),
@@ -1819,6 +1957,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
testInvalidRequest(all);
testFeatureDisabled(all);
testAMMClawbackAmount(all);
testAMMClawbackAmount(all - fixCleanup3_4_0);
testAMMClawbackAmountRoundsToZero(all);
testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0);
testAMMClawbackAll(all);
testAMMClawbackAmountSameIssuer(all);
testAMMClawbackAllSameIssuer(all);

View File

@@ -2155,6 +2155,209 @@ class AMMClawback_test : public beast::unit_test::Suite
}
BEAST_EXPECT(env.balance(carol, eur) == eur(7750));
}
// gw (USD issuer) individually freezes the AMM-USD trust line.
// AMMClawback must still succeed because the freeze invariant
// short-circuits before reaching the AMM line check (no receivers in
// the USD issuer's change set). Behavior is identical with or without
// fixCleanup3_4_0.
{
Env env(*this, features);
Account const gw{"gateway"};
Account const gw2{"gateway2"};
Account const alice{"alice"};
env.fund(XRP(1000000), gw, gw2, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
env.require(Flags(gw, asfAllowTrustLineClawback));
auto const usd = gw["USD"];
env.trust(usd(100000), alice);
env(pay(gw, alice, usd(3000)));
env.close();
auto const eur = gw2["EUR"];
env.trust(eur(100000), alice);
env(pay(gw2, alice, eur(3000)));
env.close();
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
// gw individually freezes the AMM-USD trust line (AMM pseudo-account
// <-> gw), not alice's trust line.
env(trust(gw, STAmount{Issue{usd.currency, amm.ammAccount()}, 0}, tfSetFreeze));
env.close();
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
env.close();
env.require(Balance(alice, usd(1000)));
env.require(Balance(alice, eur(2500)));
BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
}
// gw2 (EUR issuer) individually freezes the AMM-EUR trust line.
// The EUR flow (AMM → alice) is a genuine P2P transfer checked by the
// freeze invariant. Pre-fixCleanup3_4_0 the isAMMNode guard incorrectly
// blocked AMMClawback's overrideFreeze privilege on that trust line.
{
Env env(*this, features);
Account const gw{"gateway"};
Account const gw2{"gateway2"};
Account const alice{"alice"};
env.fund(XRP(1000000), gw, gw2, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
env.require(Flags(gw, asfAllowTrustLineClawback));
auto const usd = gw["USD"];
env.trust(usd(100000), alice);
env(pay(gw, alice, usd(3000)));
env.close();
auto const eur = gw2["EUR"];
env.trust(eur(100000), alice);
env(pay(gw2, alice, eur(3000)));
env.close();
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
// gw2 individually freezes the AMM-EUR trust line.
env(trust(gw2, STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, tfSetFreeze));
env.close();
if (features[fixCleanup3_4_0])
{
// Post-fixCleanup3_4_0: overrideFreeze privilege applies to
// all freeze types on AMM trust lines.
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
env.close();
env.require(Balance(alice, usd(1000)));
env.require(Balance(alice, eur(2500)));
BEAST_EXPECT(
amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
}
else
{
// Pre-fixCleanup3_4_0: the isAMMNode guard prevents the
// overrideFreeze privilege from applying to individually-frozen
// AMM trust lines, so the invariant blocks the clawback.
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
}
}
// gw2 (EUR issuer) globally freezes its issued assets. AMMClawback
// must still be able to return EUR from the AMM to alice.
{
Env env(*this, features);
Account const gw{"gateway"};
Account const gw2{"gateway2"};
Account const alice{"alice"};
env.fund(XRP(1000000), gw, gw2, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
env.require(Flags(gw, asfAllowTrustLineClawback));
auto const usd = gw["USD"];
env.trust(usd(100000), alice);
env(pay(gw, alice, usd(3000)));
env.close();
auto const eur = gw2["EUR"];
env.trust(eur(100000), alice);
env(pay(gw2, alice, eur(3000)));
env.close();
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
env(fset(gw2, asfGlobalFreeze));
env.close();
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
env.close();
env.require(Balance(alice, usd(1000)));
env.require(Balance(alice, eur(2500)));
BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
}
// Same as above but gw2 deep-freezes the AMM-EUR trust line.
if (features[featureDeepFreeze])
{
Env env(*this, features);
Account const gw{"gateway"};
Account const gw2{"gateway2"};
Account const alice{"alice"};
env.fund(XRP(1000000), gw, gw2, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
env.require(Flags(gw, asfAllowTrustLineClawback));
auto const usd = gw["USD"];
env.trust(usd(100000), alice);
env(pay(gw, alice, usd(3000)));
env.close();
auto const eur = gw2["EUR"];
env.trust(eur(100000), alice);
env(pay(gw2, alice, eur(3000)));
env.close();
AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
// gw2 deep-freezes the AMM-EUR trust line.
env(trust(
gw2,
STAmount{Issue{eur.currency, amm.ammAccount()}, 0},
tfSetFreeze | tfSetDeepFreeze));
env.close();
if (features[fixCleanup3_4_0])
{
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
env.close();
env.require(Balance(alice, usd(1000)));
env.require(Balance(alice, eur(2500)));
BEAST_EXPECT(
amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
}
else
{
// Pre-fixCleanup3_4_0: same isAMMNode guard issue blocks the
// clawback on deep-frozen AMM trust lines.
env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
}
}
}
void
@@ -2530,6 +2733,7 @@ class AMMClawback_test : public beast::unit_test::Suite
// precision loss caught in transaction layer -> tecPRECISION_LOSS
all - fixAMMClawbackRounding - featureMPTokensV2,
all - featureMPTokensV2,
all - fixCleanup3_4_0,
all})
{
testAMMClawbackSpecificAmount(features);

View File

@@ -94,6 +94,24 @@ public:
LedgerHeader const ledger3Info = env.closed()->header();
BEAST_EXPECT(ledger3Info.seq == 3);
{
// test peer non-string
auto testInvalidPeerParam = [&](auto const& param) {
json::Value params;
params[jss::account] = alice.human();
params[jss::peer] = param;
auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'.");
};
testInvalidPeerParam(1);
testInvalidPeerParam(1.1);
testInvalidPeerParam(true);
testInvalidPeerParam(json::Value(json::ValueType::Null));
testInvalidPeerParam(json::Value(json::ValueType::Object));
testInvalidPeerParam(json::Value(json::ValueType::Array));
}
{
// alice is funded but has no lines. An empty array is returned.
json::Value params;
@@ -775,6 +793,35 @@ public:
LedgerHeader const ledger3Info = env.closed()->header();
BEAST_EXPECT(ledger3Info.seq == 3);
{
// test peer non-string
auto testInvalidPeerParam = [&](auto const& param) {
json::Value params;
params[jss::account] = alice.human();
params[jss::peer] = param;
json::Value request;
request[jss::method] = "account_lines";
request[jss::jsonrpc] = "2.0";
request[jss::ripplerpc] = "2.0";
request[jss::id] = 5;
request[jss::params] = params;
auto const lines = env.rpc("json2", to_string(request));
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
};
testInvalidPeerParam(1);
testInvalidPeerParam(1.1);
testInvalidPeerParam(true);
testInvalidPeerParam(json::Value(json::ValueType::Null));
testInvalidPeerParam(json::Value(json::ValueType::Object));
testInvalidPeerParam(json::Value(json::ValueType::Array));
}
{
// alice is funded but has no lines. An empty array is returned.
json::Value params;

View File

@@ -107,7 +107,12 @@ doAccountLines(rpc::JsonContext& context)
std::string strPeer;
if (params.isMember(jss::peer))
{
if (!params[jss::peer].isString())
return rpc::invalidFieldError(jss::peer);
strPeer = params[jss::peer].asString();
}
auto const raPeerAccount = [&]() -> std::optional<AccountID> {
return strPeer.empty() ? std::nullopt : parseBase58<AccountID>(strPeer);