Files
rippled/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
Shawn Xie 768d7603b1 feat: Confidential Transfer for MPT (#5860)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: chuanshanjida <chuanshanjida@outlook.com>
Co-authored-by: Ed Hennis <ed@ripple.com>
Co-authored-by: Jingchen <a1q123456@users.noreply.github.com>
Co-authored-by: Denis Angell <dangell@transia.co>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
Co-authored-by: yinyiqian1 <yqian@ripple.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: Bronek Kozicki <brok@incorrekt.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com>
Co-authored-by: tequ <git@tequ.dev>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com>
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gregory Tsipenyuk <gregtatcam@users.noreply.github.com>
Co-authored-by: chuanshanjida <chuanshanjida@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Peter Chen <ychen@ripple.com>
Co-authored-by: Timothy Banks <timothyaaronbanks@gmail.com>
Co-authored-by: Timothy Banks <tbanks@ripple.com>
2026-06-27 01:20:38 +00:00

412 lines
12 KiB
C++

#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/digest.h>
#include <cstdint>
#include <expected>
#include <limits>
#include <set>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl {
namespace credentials {
bool
checkExpired(SLE const& sleCredential, NetClock::time_point const& closed)
{
std::uint32_t const exp =
sleCredential[~sfExpiration].value_or(std::numeric_limits<std::uint32_t>::max());
std::uint32_t const now = closed.time_since_epoch().count();
return now > exp;
}
[[nodiscard]]
static std::expected<bool, TER>
removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j)
{
auto const closeTime = view.header().parentCloseTime;
bool foundExpired = false;
for (auto const& h : arr)
{
// Credentials already checked in preclaim. Look only for expired here.
auto const k = keylet::credential(h);
auto const sleCred = view.peek(k);
if (sleCred && checkExpired(*sleCred, closeTime))
{
JLOG(j.trace()) << "Credentials are expired. Cred: " << sleCred->getText();
// delete expired credentials even if the transaction failed
auto const err = deleteSLE(view, sleCred, j);
if (view.rules().enabled(fixCleanup3_1_3) && !isTesSuccess(err))
return std::unexpected(err);
foundExpired = true;
}
}
return foundExpired;
}
TER
deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
{
if (!sleCredential)
return tecNO_ENTRY;
auto delSLE = [&view, &sleCredential, j](
AccountID const& account, SField const& node, bool isOwner) -> TER {
auto const sleAccount = view.peek(keylet::account(account));
if (!sleAccount)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Internal error: can't retrieve Owner account.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Remove object from owner directory
std::uint64_t const page = sleCredential->getFieldU64(node);
if (!view.dirRemove(keylet::ownerDir(account), page, sleCredential->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Credential from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
if (isOwner)
adjustOwnerCount(view, sleAccount, -1, j);
return tesSUCCESS;
};
auto const issuer = sleCredential->getAccountID(sfIssuer);
auto const subject = sleCredential->getAccountID(sfSubject);
bool const accepted = sleCredential->isFlag(lsfAccepted);
auto err = delSLE(issuer, sfIssuerNode, !accepted || (subject == issuer));
if (!isTesSuccess(err))
return err;
if (subject != issuer)
{
err = delSLE(subject, sfSubjectNode, accepted);
if (!isTesSuccess(err))
return err;
}
// Remove object from ledger
view.erase(sleCredential);
return tesSUCCESS;
}
NotTEC
checkFields(STTx const& tx, beast::Journal j)
{
if (!tx.isFieldPresent(sfCredentialIDs))
return tesSUCCESS;
auto const& credentials = tx.getFieldV256(sfCredentialIDs);
if (credentials.empty() || (credentials.size() > kMaxCredentialsArraySize))
{
JLOG(j.trace()) << "Malformed transaction: Credentials array size is invalid: "
<< credentials.size();
return temMALFORMED;
}
std::unordered_set<uint256> duplicates;
for (auto const& cred : credentials)
{
auto [it, ins] = duplicates.insert(cred);
if (!ins)
{
JLOG(j.trace()) << "Malformed transaction: duplicates in credentials.";
return temMALFORMED;
}
}
return tesSUCCESS;
}
TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j)
{
if (!tx.isFieldPresent(sfCredentialIDs))
return tesSUCCESS;
auto const& credIDs(tx.getFieldV256(sfCredentialIDs));
for (auto const& h : credIDs)
{
auto const sleCred = view.read(keylet::credential(h));
if (!sleCred)
{
JLOG(j.trace()) << "Credential doesn't exist. Cred: " << h;
return tecBAD_CREDENTIALS;
}
if (sleCred->getAccountID(sfSubject) != src)
{
JLOG(j.trace()) << "Credential doesn't belong to the source account. Cred: " << h;
return tecBAD_CREDENTIALS;
}
if (!sleCred->isFlag(lsfAccepted))
{
JLOG(j.trace()) << "Credential isn't accepted. Cred: " << h;
return tecBAD_CREDENTIALS;
}
// Expiration checks are in doApply
}
return tesSUCCESS;
}
TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject)
{
// Note, permissioned domain objects can be deleted at any time
auto const slePD = view.read(keylet::permissionedDomain(domainID));
if (!slePD)
return tecOBJECT_NOT_FOUND;
auto const closeTime = view.header().parentCloseTime;
bool foundExpired = false;
for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials))
{
auto const issuer = h.getAccountID(sfIssuer);
auto const type = h.getFieldVL(sfCredentialType);
auto const keyletCredential = keylet::credential(subject, issuer, makeSlice(type));
auto const sleCredential = view.read(keyletCredential);
// We cannot delete expired credentials, that would require ApplyView&
// However we can check if credentials are expired. Expected transaction
// flow is to use `validDomain` in preclaim, converting tecEXPIRED to
// tesSUCCESS, then proceed to call `verifyValidDomain` in doApply. This
// allows expired credentials to be deleted by any transaction.
if (sleCredential)
{
if (checkExpired(*sleCredential, closeTime))
{
foundExpired = true;
continue;
}
if (sleCredential->isFlag(lsfAccepted))
{
return tesSUCCESS;
}
continue;
}
}
return foundExpired ? tecEXPIRED : tecNO_AUTH;
}
TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, AccountID const& dst)
{
std::set<std::pair<AccountID, Slice>> sorted;
std::vector<SLE::const_pointer> lifeExtender;
lifeExtender.reserve(credIDs.size());
for (auto const& h : credIDs)
{
auto sleCred = view.read(keylet::credential(h));
if (!sleCred) // already checked in preclaim
return tefINTERNAL; // LCOV_EXCL_LINE
auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]);
if (!ins)
return tefINTERNAL; // LCOV_EXCL_LINE
lifeExtender.push_back(std::move(sleCred));
}
if (!view.exists(keylet::depositPreauth(dst, sorted)))
return tecNO_PERMISSION;
return tesSUCCESS;
}
std::set<std::pair<AccountID, Slice>>
makeSorted(STArray const& credentials)
{
std::set<std::pair<AccountID, Slice>> out;
for (auto const& cred : credentials)
{
auto [it, ins] = out.emplace(cred[sfIssuer], cred[sfCredentialType]);
if (!ins)
return {};
}
return out;
}
NotTEC
checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j)
{
if (credentials.empty() || (credentials.size() > maxSize))
{
JLOG(j.trace()) << "Malformed transaction: "
"Invalid credentials size: "
<< credentials.size();
return credentials.empty() ? temARRAY_EMPTY : temARRAY_TOO_LARGE;
}
std::unordered_set<uint256> duplicates;
for (auto const& credential : credentials)
{
auto const& issuer = credential[sfIssuer];
if (!issuer)
{
JLOG(j.trace()) << "Malformed transaction: "
"Issuer account is invalid: "
<< to_string(issuer);
return temINVALID_ACCOUNT_ID;
}
auto const ct = credential[sfCredentialType];
if (ct.empty() || (ct.size() > kMaxCredentialTypeLength))
{
JLOG(j.trace()) << "Malformed transaction: "
"Invalid credentialType size: "
<< ct.size();
return temMALFORMED;
}
auto [it, ins] = duplicates.insert(sha512Half(issuer, ct));
if (!ins)
{
JLOG(j.trace()) << "Malformed transaction: "
"duplicates in credentials.";
return temMALFORMED;
}
}
return tesSUCCESS;
}
} // namespace credentials
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j)
{
auto const slePD = view.read(keylet::permissionedDomain(domainID));
if (!slePD)
return tecOBJECT_NOT_FOUND;
// Collect all matching credentials on a side, so we can remove expired ones
// We may finish the loop with this collection empty, it's fine.
STVector256 credentials;
for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials))
{
auto const issuer = h.getAccountID(sfIssuer);
auto const type = h.getFieldVL(sfCredentialType);
auto const keyletCredential = keylet::credential(account, issuer, makeSlice(type));
if (view.exists(keyletCredential))
credentials.pushBack(keyletCredential.key);
}
auto const foundExpired = credentials::removeExpired(view, credentials, j);
if (!foundExpired.has_value())
return foundExpired.error();
for (auto const& h : credentials)
{
auto sleCredential = view.read(keylet::credential(h));
if (!sleCredential)
continue; // expired, i.e. deleted in credentials::removeExpired
if (sleCredential->isFlag(lsfAccepted))
return tesSUCCESS;
}
return *foundExpired ? tecEXPIRED : tecNO_PERMISSION;
}
TER
checkDepositPreauth(
STTx const& tx,
ReadView const& view,
AccountID const& src,
AccountID const& dst,
SLE::const_ref sleDst,
beast::Journal j)
{
// If depositPreauth is enabled, then an account that requires
// authorization has at least two ways to get a payment in:
// 1. If src == dst, or
// 2. If src is deposit preauthorized by dst (either by account or by
// credentials).
if (sleDst && ((sleDst->getFlags() & lsfDepositAuth) != 0u))
{
if (src != dst)
{
if (!view.exists(keylet::depositPreauth(dst, src)))
{
return !tx.isFieldPresent(sfCredentialIDs)
? tecNO_PERMISSION
: credentials::authorizedDepositPreauth(
view, tx.getFieldV256(sfCredentialIDs), dst);
}
}
}
return tesSUCCESS;
}
TER
cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j)
{
if (tx.isFieldPresent(sfCredentialIDs))
{
auto const foundExpired =
credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j);
if (!foundExpired.has_value())
return foundExpired.error();
if (*foundExpired)
return tecEXPIRED;
}
return tesSUCCESS;
}
TER
verifyDepositPreauth(
STTx const& tx,
ApplyView& view,
AccountID const& src,
AccountID const& dst,
SLE::const_ref sleDst,
beast::Journal j)
{
if (auto const err = cleanupExpiredCredentials(tx, view, j); !isTesSuccess(err))
return err;
return checkDepositPreauth(tx, view, src, dst, sleDst, j);
}
} // namespace xrpl