mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-25 16:10:57 +00:00
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>
412 lines
12 KiB
C++
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
|