Compare commits

..

8 Commits

Author SHA1 Message Date
Mayukha Vadari
249fb12e8f Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-02-27 16:41:47 -05:00
Vito Tumas
1a7f824b89 refactor: Splits invariant checks into multiple classes (#6440)
The invariant check system had grown into a single monolithic file pair containing 24 invariant checker classes. The large `InvariantCheck.cpp` file was a frequent source of merge conflicts and difficult to navigate. This refactoring improves maintainability and readability with zero behavioral changes.

In particular, this change:
- Splits `InvariantCheck.h` and `InvariantCheck.cpp` into 10 focused header/source pairs organized by domain under a new `invariants/` subdirectory.
- Extracts the shared `Privilege` enum and `hasPrivilege()` function into a dedicated `InvariantCheckPrivilege.h` header, so domain-specific files can reference them independently.
2026-02-27 21:02:39 +00:00
Mayukha Vadari
cbabee1bec Merge branch 'develop' into copilot/remove-non-canonical-fields 2026-02-27 13:44:49 -05:00
Sergey Kuznetsov
b58c681189 chore: Make nix hook optional (#6431)
This change makes the `nix` pre-commit hook optional in development environments, and enforced only inside Github Actions.
2026-02-27 13:36:10 -05:00
copilot-swe-agent[bot]
cf2835e3c1 Fix date/ctid missing from result level in API v3, fix pre-commit errors
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-02-27 17:36:19 +00:00
copilot-swe-agent[bot]
9b0e87a37e Fix: remove non-canonical fields from tx_json in API v3
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-02-27 17:02:28 +00:00
copilot-swe-agent[bot]
4a31ee1926 Initial plan 2026-02-27 16:52:00 +00:00
Mayukha Vadari
404f35d556 test: Grep for failures in CI (#6339)
This change adjusts the CI tests to make it easier to spot errors, without needing to sift through the thousands of lines of output.
2026-02-27 03:01:38 +00:00
48 changed files with 4695 additions and 4433 deletions

View File

@@ -11,7 +11,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@320be44621ca2a080f05aeb15817c44b84518108
uses: XRPLF/actions/.github/workflows/pre-commit.yml@56de1bdf19639e009639a50b8d17c28ca954f267
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'

View File

@@ -229,8 +229,21 @@ jobs:
env:
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
run: |
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}"
set -o pipefail
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
- name: Show test failure summary
if: ${{ failure() && !inputs.build_only }}
working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
run: |
if [ ! -f unittest.log ]; then
echo "unittest.log not found; embedded tests may not have run."
exit 0
fi
if ! grep -E "failed" unittest.log; then
echo "Log present but no failure lines found in unittest.log."
fi
- name: Debug failure (Linux)
if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }}
run: |

View File

@@ -61,7 +61,15 @@ repos:
hooks:
- id: nix-fmt
name: Format Nix files
entry: nix --extra-experimental-features 'nix-command flakes' fmt
entry: |
bash -c '
if command -v nix &> /dev/null || [ "$GITHUB_ACTIONS" = "true" ]; then
nix --extra-experimental-features "nix-command flakes" fmt "$@"
else
echo "Skipping nix-fmt: nix not installed and not in GitHub Actions"
exit 0
fi
' --
language: system
types:
- nix

View File

@@ -6,6 +6,13 @@ For info about how [API versioning](https://xrpl.org/request-formatting.html#api
## Breaking Changes
### Modifications to `tx` and `account_tx`
In API version 2, the `tx_json` field in `tx` and `account_tx` responses includes server-added lower-case fields (`date`, `ledger_index`, and `ctid`) that are not part of the canonical signed transaction. In API version 3, these fields are removed from `tx_json` and are only present at the top-level result object.
- **Before (API v2)**: The `tx_json` object in the response contained `date`, `ledger_index`, and `ctid` fields alongside the canonical PascalCase transaction fields.
- **After (API v3)**: The `tx_json` object contains only the canonical signed transaction fields. The `date`, `ledger_index`, and `ctid` fields appear exclusively at the top-level result object.
### Modifications to `amm_info`
The order of error checks has been changed to provide more specific error messages. ([#4924](https://github.com/XRPLF/rippled/pull/4924))

View File

@@ -23,9 +23,10 @@ struct JsonOptions
none = 0b0000'0000,
include_date = 0b0000'0001,
disable_API_prior_V2 = 0b0000'0010,
disable_API_prior_V3 = 0b0000'0100,
// IMPORTANT `_all` must be union of all of the above; see also operator~
_all = 0b0000'0011
_all = 0b0000'0111
// clang-format on
};

View File

@@ -15,9 +15,10 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(BatchV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
@@ -31,6 +32,7 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Batch, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions

View File

@@ -918,7 +918,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
#endif
TRANSACTION(ttBATCH, 71, Batch,
Delegation::notDelegable,
featureBatchV1_1,
featureBatch,
noPriv,
({
{sfRawTransactions, soeREQUIRED},

View File

@@ -1,732 +0,0 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <tuple>
#include <unordered_set>
namespace xrpl {
class ReadView;
#if GENERATING_DOCS
/**
* @brief Prototype for invariant check implementations.
*
* __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to
* communicate the interface required of any invariant checker. Any invariant
* check implementation should implement the public methods documented here.
*
*/
class InvariantChecker_PROTOTYPE
{
public:
explicit InvariantChecker_PROTOTYPE() = default;
/**
* @brief called for each ledger entry in the current transaction.
*
* @param isDelete true if the SLE is being deleted
* @param before ledger entry before modification by the transaction
* @param after ledger entry after modification by the transaction
*/
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/**
* @brief called after all ledger entries have been visited to determine
* the final status of the check
*
* @param tx the transaction being applied
* @param tec the current TER result of the transaction
* @param fee the fee actually charged for this transaction
* @param view a ReadView of the ledger being modified
* @param j journal for logging
*
* @return true if check passes, false if it fails
*/
bool
finalize(
STTx const& tx,
TER const tec,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
};
#endif
/**
* @brief Invariant: We should never charge a transaction a negative fee or a
* fee that is larger than what the transaction itself specifies.
*
* We can, in some circumstances, charge less.
*/
class TransactionFeeCheck
{
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: A transaction must not create XRP and should only destroy
* the XRP fee.
*
* We iterate through all account roots, payment channels and escrow entries
* that were modified and calculate the net change in XRP caused by the
* transactions.
*/
class XRPNotCreated
{
std::int64_t drops_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: we cannot remove an account ledger entry
*
* We iterate all account roots that were modified, and ensure that any that
* were present before the transaction was applied continue to be present
* afterwards unless they were explicitly deleted by a successful
* AccountDelete transaction.
*/
class AccountRootsNotDeleted
{
std::uint32_t accountsDeleted_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: a deleted account must not have any objects left
*
* We iterate all deleted account roots, and ensure that there are no
* objects left that are directly accessible with that account's ID.
*
* There should only be one deleted account, but that's checked by
* AccountRootsNotDeleted. This invariant will handle multiple deleted account
* roots without a problem.
*/
class AccountRootsDeletedClean
{
// Pair is <before, after>. Before is used for most of the checks, so that
// if, for example, an object ID field is cleared, but the object is not
// deleted, it can still be found. After is used specifically for any checks
// that are expected as part of the deletion, such as zeroing out the
// balance.
std::vector<std::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>> accountsDeleted_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: An account XRP balance must be in XRP and take a value
* between 0 and INITIAL_XRP drops, inclusive.
*
* We iterate all account roots modified by the transaction and ensure that
* their XRP balances are reasonable.
*/
class XRPBalanceChecks
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: corresponding modified ledger entries should match in type
* and added entries should be a valid type.
*/
class LedgerEntryTypesMatch
{
bool typeMismatch_ = false;
bool invalidTypeAdded_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Trust lines using XRP are not allowed.
*
* We iterate all the trust lines created by this transaction and ensure
* that they are against a valid issuer.
*/
class NoXRPTrustLines
{
bool xrpTrustLine_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Trust lines with deep freeze flag are not allowed if normal
* freeze flag is not set.
*
* We iterate all the trust lines created by this transaction and ensure
* that they don't have deep freeze flag set without normal freeze flag set.
*/
class NoDeepFreezeTrustLinesWithoutFreeze
{
bool deepFreezeWithoutFreeze_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: frozen trust line balance change is not allowed.
*
* We iterate all affected trust lines and ensure that they don't have
* unexpected change of balance if they're frozen.
*/
class TransfersNotFrozen
{
struct BalanceChange
{
std::shared_ptr<SLE const> const line;
int const balanceChangeSign;
};
struct IssuerChanges
{
std::vector<BalanceChange> senders;
std::vector<BalanceChange> receivers;
};
using ByIssuer = std::map<Issue, IssuerChanges>;
ByIssuer balanceChanges_;
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
bool
isValidEntry(std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
STAmount
calculateBalanceChange(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after,
bool isDelete);
void
recordBalance(Issue const& issue, BalanceChange change);
void
recordBalanceChanges(std::shared_ptr<SLE const> const& after, STAmount const& balanceChange);
std::shared_ptr<SLE const>
findIssuer(AccountID const& issuerID, ReadView const& view);
bool
validateIssuerChanges(
std::shared_ptr<SLE const> const& issuer,
IssuerChanges const& changes,
STTx const& tx,
beast::Journal const& j,
bool enforce);
bool
validateFrozenState(
BalanceChange const& change,
bool high,
STTx const& tx,
beast::Journal const& j,
bool enforce,
bool globalFreeze);
};
/**
* @brief Invariant: offers should be for non-negative amounts and must not
* be XRP to XRP.
*
* Examine all offers modified by the transaction and ensure that there are
* no offers which contain negative amounts or which exchange XRP for XRP.
*/
class NoBadOffers
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: an escrow entry must take a value between 0 and
* INITIAL_XRP drops exclusive.
*/
class NoZeroEscrow
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: a new account root must be the consequence of a payment,
* must have the right starting sequence, and the payment
* may not create more than one new account root.
*/
class ValidNewAccountRoot
{
std::uint32_t accountsCreated_ = 0;
std::uint32_t accountSeq_ = 0;
bool pseudoAccount_ = false;
std::uint32_t flags_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Validates several invariants for NFToken pages.
*
* The following checks are made:
* - The page is correctly associated with the owner.
* - The page is correctly ordered between the next and previous links.
* - The page contains at least one and no more than 32 NFTokens.
* - The NFTokens on this page do not belong on a lower or higher page.
* - The NFTokens are correctly sorted on the page.
* - Each URI, if present, is not empty.
*/
class ValidNFTokenPage
{
bool badEntry_ = false;
bool badLink_ = false;
bool badSort_ = false;
bool badURI_ = false;
bool invalidSize_ = false;
bool deletedFinalPage_ = false;
bool deletedLink_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Validates counts of NFTokens after all transaction types.
*
* The following checks are made:
* - The number of minted or burned NFTokens can only be changed by
* NFTokenMint or NFTokenBurn transactions.
* - A successful NFTokenMint must increase the number of NFTokens.
* - A failed NFTokenMint must not change the number of minted NFTokens.
* - An NFTokenMint transaction cannot change the number of burned NFTokens.
* - A successful NFTokenBurn must increase the number of burned NFTokens.
* - A failed NFTokenBurn must not change the number of burned NFTokens.
* - An NFTokenBurn transaction cannot change the number of minted NFTokens.
*/
class NFTokenCountTracking
{
std::uint32_t beforeMintedTotal = 0;
std::uint32_t beforeBurnedTotal = 0;
std::uint32_t afterMintedTotal = 0;
std::uint32_t afterBurnedTotal = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Token holder's trustline balance cannot be negative after
* Clawback.
*
* We iterate all the trust lines affected by this transaction and ensure
* that no more than one trustline is modified, and also holder's balance is
* non-negative.
*/
class ValidClawback
{
std::uint32_t trustlinesChanged = 0;
std::uint32_t mptokensChanged = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
class ValidMPTIssuance
{
std::uint32_t mptIssuancesCreated_ = 0;
std::uint32_t mptIssuancesDeleted_ = 0;
std::uint32_t mptokensCreated_ = 0;
std::uint32_t mptokensDeleted_ = 0;
// non-MPT transactions may attempt to create
// MPToken by an issuer
bool mptCreatedByIssuer_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Permissioned Domains must have some rules and
* AcceptedCredentials must have length between 1 and 10 inclusive.
*
* Since only permissions constitute rules, an empty credentials list
* means that there are no rules and the invariant is violated.
*
* Credentials must be sorted and no duplicates allowed
*
*/
class ValidPermissionedDomain
{
struct SleStatus
{
std::size_t credentialsSize_{0};
bool isSorted_ = false;
bool isUnique_ = false;
bool isDelete_ = false;
};
std::vector<SleStatus> sleStatus_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Pseudo-accounts have valid and consistent properties
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
class ValidPermissionedDEX
{
bool regularOffers_ = false;
bool badHybrids_ = false;
hash_set<uint256> domains_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
class ValidAMM
{
std::optional<AccountID> ammAccount_;
std::optional<STAmount> lptAMMBalanceAfter_;
std::optional<STAmount> lptAMMBalanceBefore_;
bool ammPoolChanged_;
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
ValidAMM() : ammPoolChanged_{false}
{
}
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
bool
finalizeBid(bool enforce, beast::Journal const&) const;
bool
finalizeVote(bool enforce, beast::Journal const&) const;
bool
finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
bool
finalizeDelete(bool enforce, TER res, beast::Journal const&) const;
bool
finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
// Includes clawback
bool
finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
bool
finalizeDEX(bool enforce, beast::Journal const&) const;
bool
generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&)
const;
};
/**
* @brief Invariants: Some fields are unmodifiable
*
* Check that any fields specified as unmodifiable are not modified when the
* object is modified. Creation and deletion are ignored.
*
*/
class NoModifiedUnmodifiableFields
{
// Pair is <before, after>.
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Loan brokers are internally consistent
*
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
* node (the root), which will only hold entries for `RippleState` or
* `MPToken` objects.
*
*/
class ValidLoanBroker
{
// Not all of these elements will necessarily be populated. Remaining items
// will be looked up as needed.
struct BrokerInfo
{
SLE::const_pointer brokerBefore = nullptr;
// After is used for most of the checks, except
// those that check changed values.
SLE::const_pointer brokerAfter = nullptr;
};
// Collect all the LoanBrokers found directly or indirectly through
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
// LoanBroker object if brokerBefore and brokerAfter are nullptr
std::map<uint256, BrokerInfo> brokers_;
// Collect all the modified trust lines. Their high and low accounts will be
// loaded to look for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> lines_;
// Collect all the modified MPTokens. Their accounts will be loaded to look
// for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> mpts_;
bool
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Loans are internally consistent
*
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
*
*/
class ValidLoan
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/*
* @brief Invariants: Vault object and MPTokenIssuance for vault shares
*
* - vault deleted and vault created is empty
* - vault created must be linked to pseudo-account for shares and assets
* - vault must have MPTokenIssuance for shares
* - vault without shares outstanding must have no shares
* - loss unrealized does not exceed the difference between assets total and
* assets available
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
*
*/
class ValidVault
{
Number static constexpr zero{};
struct Vault final
{
uint256 key = beast::zero;
Asset asset = {};
AccountID pseudoId = {};
AccountID owner = {};
uint192 shareMPTID = beast::zero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Vault static make(SLE const&);
};
struct Shares final
{
MPTIssue share = {};
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
Shares static make(SLE const&);
};
std::vector<Vault> afterVault_ = {};
std::vector<Shares> afterMPTs_ = {};
std::vector<Vault> beforeVault_ = {};
std::vector<Shares> beforeMPTs_ = {};
std::unordered_map<uint256, Number> deltas_ = {};
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
TransactionFeeCheck,
AccountRootsNotDeleted,
AccountRootsDeletedClean,
LedgerEntryTypesMatch,
XRPBalanceChecks,
XRPNotCreated,
NoXRPTrustLines,
NoDeepFreezeTrustLinesWithoutFreeze,
TransfersNotFrozen,
NoBadOffers,
NoZeroEscrow,
ValidNewAccountRoot,
ValidNFTokenPage,
NFTokenCountTracking,
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain,
ValidPermissionedDEX,
ValidAMM,
NoModifiedUnmodifiableFields,
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault>;
/**
* @brief get a tuple of all invariant checks
*
* @return std::tuple of instances that implement the required invariant check
* methods
*
* @see xrpl::InvariantChecker_PROTOTYPE
*/
inline InvariantChecks
getInvariantChecks()
{
return InvariantChecks{};
}
} // namespace xrpl

View File

@@ -162,6 +162,9 @@ public:
static NotTEC
checkSign(PreclaimContext const& ctx);
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// Returns the fee in fee units, not scaled for load.
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
@@ -290,7 +293,14 @@ protected:
std::optional<T> value,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
protected:
private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
static NotTEC
checkSingleSign(
ReadView const& view,
@@ -306,15 +316,6 @@ protected:
STObject const& sigObject,
beast::Journal const j);
private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
void trapTransaction(uint256) const;
/** Performs early sanity checks on the account and fee fields.

View File

@@ -0,0 +1,53 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <optional>
namespace xrpl {
class ValidAMM
{
std::optional<AccountID> ammAccount_;
std::optional<STAmount> lptAMMBalanceAfter_;
std::optional<STAmount> lptAMMBalanceBefore_;
bool ammPoolChanged_;
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
ValidAMM() : ammPoolChanged_{false}
{
}
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
bool
finalizeBid(bool enforce, beast::Journal const&) const;
bool
finalizeVote(bool enforce, beast::Journal const&) const;
bool
finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
bool
finalizeDelete(bool enforce, TER res, beast::Journal const&) const;
bool
finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
// Includes clawback
bool
finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
bool
finalizeDEX(bool enforce, beast::Journal const&) const;
bool
generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&)
const;
};
} // namespace xrpl

View File

@@ -0,0 +1,84 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <map>
#include <vector>
namespace xrpl {
/**
* @brief Invariant: frozen trust line balance change is not allowed.
*
* We iterate all affected trust lines and ensure that they don't have
* unexpected change of balance if they're frozen.
*/
class TransfersNotFrozen
{
struct BalanceChange
{
std::shared_ptr<SLE const> const line;
int const balanceChangeSign;
};
struct IssuerChanges
{
std::vector<BalanceChange> senders;
std::vector<BalanceChange> receivers;
};
using ByIssuer = std::map<Issue, IssuerChanges>;
ByIssuer balanceChanges_;
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
bool
isValidEntry(std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
STAmount
calculateBalanceChange(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after,
bool isDelete);
void
recordBalance(Issue const& issue, BalanceChange change);
void
recordBalanceChanges(std::shared_ptr<SLE const> const& after, STAmount const& balanceChange);
std::shared_ptr<SLE const>
findIssuer(AccountID const& issuerID, ReadView const& view);
bool
validateIssuerChanges(
std::shared_ptr<SLE const> const& issuer,
IssuerChanges const& changes,
STTx const& tx,
beast::Journal const& j,
bool enforce);
bool
validateFrozenState(
BalanceChange const& change,
bool high,
STTx const& tx,
beast::Journal const& j,
bool enforce,
bool globalFreeze);
};
} // namespace xrpl

View File

@@ -0,0 +1,385 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/LoanInvariant.h>
#include <xrpl/tx/invariants/MPTInvariant.h>
#include <xrpl/tx/invariants/NFTInvariant.h>
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
#include <xrpl/tx/invariants/VaultInvariant.h>
#include <cstdint>
#include <tuple>
namespace xrpl {
#if GENERATING_DOCS
/**
* @brief Prototype for invariant check implementations.
*
* __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to
* communicate the interface required of any invariant checker. Any invariant
* check implementation should implement the public methods documented here.
*
*/
class InvariantChecker_PROTOTYPE
{
public:
explicit InvariantChecker_PROTOTYPE() = default;
/**
* @brief called for each ledger entry in the current transaction.
*
* @param isDelete true if the SLE is being deleted
* @param before ledger entry before modification by the transaction
* @param after ledger entry after modification by the transaction
*/
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/**
* @brief called after all ledger entries have been visited to determine
* the final status of the check
*
* @param tx the transaction being applied
* @param tec the current TER result of the transaction
* @param fee the fee actually charged for this transaction
* @param view a ReadView of the ledger being modified
* @param j journal for logging
*
* @return true if check passes, false if it fails
*/
bool
finalize(
STTx const& tx,
TER const tec,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j);
};
#endif
/**
* @brief Invariant: We should never charge a transaction a negative fee or a
* fee that is larger than what the transaction itself specifies.
*
* We can, in some circumstances, charge less.
*/
class TransactionFeeCheck
{
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: A transaction must not create XRP and should only destroy
* the XRP fee.
*
* We iterate through all account roots, payment channels and escrow entries
* that were modified and calculate the net change in XRP caused by the
* transactions.
*/
class XRPNotCreated
{
std::int64_t drops_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: we cannot remove an account ledger entry
*
* We iterate all account roots that were modified, and ensure that any that
* were present before the transaction was applied continue to be present
* afterwards unless they were explicitly deleted by a successful
* AccountDelete transaction.
*/
class AccountRootsNotDeleted
{
std::uint32_t accountsDeleted_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: a deleted account must not have any objects left
*
* We iterate all deleted account roots, and ensure that there are no
* objects left that are directly accessible with that account's ID.
*
* There should only be one deleted account, but that's checked by
* AccountRootsNotDeleted. This invariant will handle multiple deleted account
* roots without a problem.
*/
class AccountRootsDeletedClean
{
// Pair is <before, after>. Before is used for most of the checks, so that
// if, for example, an object ID field is cleared, but the object is not
// deleted, it can still be found. After is used specifically for any checks
// that are expected as part of the deletion, such as zeroing out the
// balance.
std::vector<std::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>> accountsDeleted_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: An account XRP balance must be in XRP and take a value
* between 0 and INITIAL_XRP drops, inclusive.
*
* We iterate all account roots modified by the transaction and ensure that
* their XRP balances are reasonable.
*/
class XRPBalanceChecks
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: corresponding modified ledger entries should match in type
* and added entries should be a valid type.
*/
class LedgerEntryTypesMatch
{
bool typeMismatch_ = false;
bool invalidTypeAdded_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Trust lines using XRP are not allowed.
*
* We iterate all the trust lines created by this transaction and ensure
* that they are against a valid issuer.
*/
class NoXRPTrustLines
{
bool xrpTrustLine_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Trust lines with deep freeze flag are not allowed if normal
* freeze flag is not set.
*
* We iterate all the trust lines created by this transaction and ensure
* that they don't have deep freeze flag set without normal freeze flag set.
*/
class NoDeepFreezeTrustLinesWithoutFreeze
{
bool deepFreezeWithoutFreeze_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: offers should be for non-negative amounts and must not
* be XRP to XRP.
*
* Examine all offers modified by the transaction and ensure that there are
* no offers which contain negative amounts or which exchange XRP for XRP.
*/
class NoBadOffers
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: an escrow entry must take a value between 0 and
* INITIAL_XRP drops exclusive.
*/
class NoZeroEscrow
{
bool bad_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: a new account root must be the consequence of a payment,
* must have the right starting sequence, and the payment
* may not create more than one new account root.
*/
class ValidNewAccountRoot
{
std::uint32_t accountsCreated_ = 0;
std::uint32_t accountSeq_ = 0;
bool pseudoAccount_ = false;
std::uint32_t flags_ = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Token holder's trustline balance cannot be negative after
* Clawback.
*
* We iterate all the trust lines affected by this transaction and ensure
* that no more than one trustline is modified, and also holder's balance is
* non-negative.
*/
class ValidClawback
{
std::uint32_t trustlinesChanged = 0;
std::uint32_t mptokensChanged = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Pseudo-accounts have valid and consistent properties
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Some fields are unmodifiable
*
* Check that any fields specified as unmodifiable are not modified when the
* object is modified. Creation and deletion are ignored.
*
*/
class NoModifiedUnmodifiableFields
{
// Pair is <before, after>.
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
TransactionFeeCheck,
AccountRootsNotDeleted,
AccountRootsDeletedClean,
LedgerEntryTypesMatch,
XRPBalanceChecks,
XRPNotCreated,
NoXRPTrustLines,
NoDeepFreezeTrustLinesWithoutFreeze,
TransfersNotFrozen,
NoBadOffers,
NoZeroEscrow,
ValidNewAccountRoot,
ValidNFTokenPage,
NFTokenCountTracking,
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain,
ValidPermissionedDEX,
ValidAMM,
NoModifiedUnmodifiableFields,
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault>;
/**
* @brief get a tuple of all invariant checks
*
* @return std::tuple of instances that implement the required invariant check
* methods
*
* @see xrpl::InvariantChecker_PROTOTYPE
*/
inline InvariantChecks
getInvariantChecks()
{
return InvariantChecks{};
}
} // namespace xrpl

View File

@@ -0,0 +1,60 @@
#pragma once
#include <xrpl/protocol/STTx.h>
#include <type_traits>
namespace xrpl {
/*
assert(enforce)
There are several asserts (or XRPL_ASSERTs) in invariant check files that check
a variable named `enforce` when an invariant fails. At first glance, those
asserts may look incorrect, but they are not.
Those asserts take advantage of two facts:
1. `asserts` are not (normally) executed in release builds.
2. Invariants should *never* fail, except in tests that specifically modify
the open ledger to break them.
This makes `assert(enforce)` sort of a second-layer of invariant enforcement
aimed at _developers_. It's designed to fire if a developer writes code that
violates an invariant, and runs it in unit tests or a develop build that _does
not have the relevant amendments enabled_. It's intentionally a pain in the neck
so that bad code gets caught and fixed as early as possible.
*/
enum Privilege {
noPriv = 0x0000, // The transaction can not do any of the enumerated operations
createAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
createPseudoAcct = 0x0002, // The transaction can create a pseudo account,
// which implies createAcct
mustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
// object, but does not have to
overrideFreeze = 0x0010, // The transaction can override some freeze rules
changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT
createMPTIssuance = 0x0040, // The transaction can create a new MPT issuance
destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance
mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT
// object (except by issuer)
mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT
// object (except by issuer)
mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create.
mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
};
constexpr Privilege
operator|(Privilege lhs, Privilege rhs)
{
return safe_cast<Privilege>(
safe_cast<std::underlying_type_t<Privilege>>(lhs) |
safe_cast<std::underlying_type_t<Privilege>>(rhs));
}
bool
hasPrivilege(STTx const& tx, Privilege priv);
} // namespace xrpl

View File

@@ -0,0 +1,75 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <map>
#include <vector>
namespace xrpl {
/**
* @brief Invariants: Loan brokers are internally consistent
*
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
* node (the root), which will only hold entries for `RippleState` or
* `MPToken` objects.
*
*/
class ValidLoanBroker
{
// Not all of these elements will necessarily be populated. Remaining items
// will be looked up as needed.
struct BrokerInfo
{
SLE::const_pointer brokerBefore = nullptr;
// After is used for most of the checks, except
// those that check changed values.
SLE::const_pointer brokerAfter = nullptr;
};
// Collect all the LoanBrokers found directly or indirectly through
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
// LoanBroker object if brokerBefore and brokerAfter are nullptr
std::map<uint256, BrokerInfo> brokers_;
// Collect all the modified trust lines. Their high and low accounts will be
// loaded to look for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> lines_;
// Collect all the modified MPTokens. Their accounts will be loaded to look
// for LoanBroker pseudo-accounts.
std::vector<SLE::const_pointer> mpts_;
bool
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j) const;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Loans are internally consistent
*
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
*
*/
class ValidLoan
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,31 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
namespace xrpl {
class ValidMPTIssuance
{
std::uint32_t mptIssuancesCreated_ = 0;
std::uint32_t mptIssuancesDeleted_ = 0;
std::uint32_t mptokensCreated_ = 0;
std::uint32_t mptokensDeleted_ = 0;
// non-MPT transactions may attempt to create
// MPToken by an issuer
bool mptCreatedByIssuer_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,70 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
namespace xrpl {
/**
* @brief Invariant: Validates several invariants for NFToken pages.
*
* The following checks are made:
* - The page is correctly associated with the owner.
* - The page is correctly ordered between the next and previous links.
* - The page contains at least one and no more than 32 NFTokens.
* - The NFTokens on this page do not belong on a lower or higher page.
* - The NFTokens are correctly sorted on the page.
* - Each URI, if present, is not empty.
*/
class ValidNFTokenPage
{
bool badEntry_ = false;
bool badLink_ = false;
bool badSort_ = false;
bool badURI_ = false;
bool invalidSize_ = false;
bool deletedFinalPage_ = false;
bool deletedLink_ = false;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: Validates counts of NFTokens after all transaction types.
*
* The following checks are made:
* - The number of minted or burned NFTokens can only be changed by
* NFTokenMint or NFTokenBurn transactions.
* - A successful NFTokenMint must increase the number of NFTokens.
* - A failed NFTokenMint must not change the number of minted NFTokens.
* - An NFTokenMint transaction cannot change the number of burned NFTokens.
* - A successful NFTokenBurn must increase the number of burned NFTokens.
* - A failed NFTokenBurn must not change the number of burned NFTokens.
* - An NFTokenBurn transaction cannot change the number of minted NFTokens.
*/
class NFTokenCountTracking
{
std::uint32_t beforeMintedTotal = 0;
std::uint32_t beforeBurnedTotal = 0;
std::uint32_t afterMintedTotal = 0;
std::uint32_t afterBurnedTotal = 0;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,25 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
namespace xrpl {
class ValidPermissionedDEX
{
bool regularOffers_ = false;
bool badHybrids_ = false;
hash_set<uint256> domains_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,41 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <vector>
namespace xrpl {
/**
* @brief Invariants: Permissioned Domains must have some rules and
* AcceptedCredentials must have length between 1 and 10 inclusive.
*
* Since only permissions constitute rules, an empty credentials list
* means that there are no rules and the invariant is violated.
*
* Credentials must be sorted and no duplicates allowed
*
*/
class ValidPermissionedDomain
{
struct SleStatus
{
std::size_t credentialsSize_{0};
bool isSorted_ = false;
bool isUnique_ = false;
bool isDelete_ = false;
};
std::vector<SleStatus> sleStatus_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -0,0 +1,77 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <unordered_map>
#include <vector>
namespace xrpl {
/*
* @brief Invariants: Vault object and MPTokenIssuance for vault shares
*
* - vault deleted and vault created is empty
* - vault created must be linked to pseudo-account for shares and assets
* - vault must have MPTokenIssuance for shares
* - vault without shares outstanding must have no shares
* - loss unrealized does not exceed the difference between assets total and
* assets available
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
*
*/
class ValidVault
{
Number static constexpr zero{};
struct Vault final
{
uint256 key = beast::zero;
Asset asset = {};
AccountID pseudoId = {};
AccountID owner = {};
uint192 shareMPTID = beast::zero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Vault static make(SLE const&);
};
struct Shares final
{
MPTIssue share = {};
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
Shares static make(SLE const&);
};
std::vector<Vault> afterVault_ = {};
std::vector<Shares> afterMPTs_ = {};
std::vector<Vault> beforeVault_ = {};
std::vector<Shares> beforeMPTs_ = {};
std::unordered_map<uint256, Number> deltas_ = {};
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
} // namespace xrpl

View File

@@ -27,9 +27,6 @@ public:
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
static NotTEC
checkSign(PreclaimContext const& ctx);

View File

@@ -278,8 +278,6 @@ STTx::checkBatchSign(Rules const& rules) const
JLOG(debugLog().fatal()) << "not a batch transaction";
return Unexpected("Not a batch transaction.");
}
if (!isFieldPresent(sfBatchSigners))
return Unexpected("Missing BatchSigners field.");
STArray const& signers{getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
@@ -294,8 +292,9 @@ STTx::checkBatchSign(Rules const& rules) const
}
catch (std::exception const& e)
{
return Unexpected(std::string("Internal batch signature check failure: ") + e.what());
JLOG(debugLog().error()) << "Batch signature check failed: " << e.what();
}
return Unexpected("Internal batch signature check failure.");
}
Json::Value
@@ -417,7 +416,6 @@ STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
Serializer msg;
serializeBatch(msg, getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}
@@ -490,7 +488,7 @@ multiSignHelper(
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") + toBase58(accountID) +
(errorWhat ? ": " + *errorWhat : "") + ".");
errorWhat.value_or("") + ".");
}
// All signatures verified.
return {};

View File

@@ -1,8 +1,9 @@
#include <xrpl/tx/ApplyContext.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/to_string.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/InvariantCheck.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
namespace xrpl {

File diff suppressed because it is too large Load Diff

View File

@@ -175,12 +175,12 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID;
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch))
return temINVALID_FLAG;
XRPL_ASSERT(
ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() ||
!ctx.rules.enabled(featureBatchV1_1),
!ctx.rules.enabled(featureBatch),
"Inner batch transaction must have a parent batch ID.");
return tesSUCCESS;
@@ -196,13 +196,13 @@ Transactor::preflight2(PreflightContext const& ctx)
return *ret;
// It should be impossible for the InnerBatchTxn flag to be set without
// featureBatchV1_1 being enabled
// featureBatch being enabled
XRPL_ASSERT_PARTS(
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatchV1_1),
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch),
"xrpl::Transactor::preflight2",
"InnerBatch flag only set if feature enabled");
// Skip signature check on batch inner transactions
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1))
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch))
return tesSUCCESS;
// Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped.
@@ -647,7 +647,7 @@ Transactor::checkSign(
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions
if (parentBatchId && view.rules().enabled(featureBatchV1_1))
if (parentBatchId && view.rules().enabled(featureBatch))
{
// Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
@@ -699,6 +699,50 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}
NotTEC
Transactor::checkBatchSign(PreclaimContext const& ctx)
{
NotTEC ret = tesSUCCESS;
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
auto const idAccount = signer.getAccountID(sfAccount);
Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
// LCOV_EXCL_START
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH;
// LCOV_EXCL_STOP
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
// A batch can include transactions from an un-created account ONLY
// when the account master key is the signer
if (!sleAccount)
{
if (idAccount != idSigner)
return tefBAD_AUTH;
return tesSUCCESS;
}
if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
!isTesSuccess(ret))
return ret;
}
}
return ret;
}
NotTEC
Transactor::checkSingleSign(
ReadView const& view,

View File

@@ -24,12 +24,29 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
auto const flags = router.getFlags(id);
// Ignore signature check on batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatchV1_1))
if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch))
{
// Defensive Check: These values are also checked in Batch::preflight
if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() ||
tx.isFieldPresent(sfSigners))
return {Validity::SigBad, "Malformed: Invalid inner batch transaction."};
// This block should probably have never been included in the
// original `Batch` implementation. An inner transaction never
// has a valid signature.
bool const neverValid = rules.enabled(fixBatchInnerSigs);
if (!neverValid)
{
std::string reason;
if (!passesLocalChecks(tx, reason))
{
router.setFlags(id, SF_LOCALBAD);
return {Validity::SigGoodOnly, reason};
}
router.setFlags(id, SF_SIGGOOD);
return {Validity::Valid, ""};
}
}
if (any(flags & SF_SIGBAD))

View File

@@ -0,0 +1,305 @@
#include <xrpl/tx/invariants/AMMInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/transactors/AMM/AMMHelpers.h>
#include <xrpl/tx/transactors/AMM/AMMUtils.h>
namespace xrpl {
void
ValidAMM::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
return;
if (after)
{
auto const type = after->getType();
// AMM object changed
if (type == ltAMM)
{
ammAccount_ = after->getAccountID(sfAccount);
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
}
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
{
ammPoolChanged_ = true;
}
}
if (before)
{
// AMM object changed
if (before->getType() == ltAMM)
{
lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
}
}
}
static bool
validBalances(
STAmount const& amount,
STAmount const& amount2,
STAmount const& lptAMMBalance,
ValidAMM::ZeroAllowed zeroAllowed)
{
bool const positive =
amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero;
if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
return positive ||
(amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero);
return positive;
}
bool
ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
{
if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_)
{
// LPTokens and the pool can not change on vote
// LCOV_EXCL_START
JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{})
<< " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " "
<< ammPoolChanged_;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
{
if (ammPoolChanged_)
{
// The pool can not change on bid
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: pool changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
// LPTokens are burnt, therefore there should be fewer LPTokens
else if (
lptAMMBalanceBefore_ && lptAMMBalanceAfter_ &&
(*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero))
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " "
<< *lptAMMBalanceAfter_;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeCreate(
STTx const& tx,
ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
else
{
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAmount].get<Issue>(),
tx[sfAmount2].get<Issue>(),
fhIGNORE_FREEZE,
j);
// Create invariant:
// sqrt(amount * amount2) == LPTokens
// all balances are greater than zero
if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_)
{
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
<< *lptAMMBalanceAfter_;
if (enforce)
return false;
}
}
return true;
}
bool
ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
{
if (ammAccount_)
{
// LCOV_EXCL_START
std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS"
: "AMM object is changed on tecINCOMPLETE";
JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
{
if (ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::generalInvariant(
xrpl::STTx const& tx,
xrpl::ReadView const& view,
ZeroAllowed zeroAllowed,
beast::Journal const& j) const
{
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAsset].get<Issue>(),
tx[sfAsset2].get<Issue>(),
fhIGNORE_FREEZE,
j);
// Deposit and Withdrawal invariant:
// sqrt(amount * amount2) >= LPTokens
// all balances are greater than zero
// unless on last withdrawal
auto const poolProductMean = root2(amount * amount2);
bool const nonNegativeBalances =
validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
// Allow for a small relative error if strongInvariantCheck fails
auto weakInvariantCheck = [&]() {
return *lptAMMBalanceAfter_ != beast::zero &&
withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
};
if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
{
JLOG(j.error()) << "AMM " << tx.getTxnType()
<< " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " "
<< ammPoolChanged_ << " " << amount << " " << amount2 << " "
<< poolProductMean << " " << lptAMMBalanceAfter_->getText() << " "
<< ((*lptAMMBalanceAfter_ == beast::zero)
? Number{1}
: ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean));
return false;
}
return true;
}
bool
ValidAMM::finalizeDeposit(
xrpl::STTx const& tx,
xrpl::ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
return false;
return true;
}
bool
ValidAMM::finalizeWithdraw(
xrpl::STTx const& tx,
xrpl::ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// Last Withdraw or Clawback deleted AMM
}
else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
{
if (enforce)
return false;
}
return true;
}
bool
ValidAMM::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Delete may return tecINCOMPLETE if there are too many
// trustlines to delete.
if (result != tesSUCCESS && result != tecINCOMPLETE)
return true;
bool const enforce = view.rules().enabled(fixAMMv1_3);
switch (tx.getTxnType())
{
case ttAMM_CREATE:
return finalizeCreate(tx, view, enforce, j);
case ttAMM_DEPOSIT:
return finalizeDeposit(tx, view, enforce, j);
case ttAMM_CLAWBACK:
case ttAMM_WITHDRAW:
return finalizeWithdraw(tx, view, enforce, j);
case ttAMM_BID:
return finalizeBid(enforce, j);
case ttAMM_VOTE:
return finalizeVote(enforce, j);
case ttAMM_DELETE:
return finalizeDelete(enforce, result, j);
case ttCHECK_CASH:
case ttOFFER_CREATE:
case ttPAYMENT:
return finalizeDEX(enforce, j);
default:
break;
}
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,278 @@
#include <xrpl/tx/invariants/FreezeInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
namespace xrpl {
void
TransfersNotFrozen::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
/*
* A trust line freeze state alone doesn't determine if a transfer is
* frozen. The transfer must be examined "end-to-end" because both sides of
* the transfer may have different freeze states and freeze impact depends
* on the transfer direction. This is why first we need to track the
* transfers using IssuerChanges senders/receivers.
*
* Only in validateIssuerChanges, after we collected all changes can we
* determine if the transfer is valid.
*/
if (!isValidEntry(before, after))
{
return;
}
auto const balanceChange = calculateBalanceChange(before, after, isDelete);
if (balanceChange.signum() == 0)
{
return;
}
recordBalanceChanges(after, balanceChange);
}
bool
TransfersNotFrozen::finalize(
STTx const& tx,
TER const ter,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j)
{
/*
* We check this invariant regardless of deep freeze amendment status,
* allowing for detection and logging of potential issues even when the
* amendment is disabled.
*
* If an exploit that allows moving frozen assets is discovered,
* we can alert operators who monitor fatal messages and trigger assert in
* debug builds for an early warning.
*
* In an unlikely event that an exploit is found, this early detection
* enables encouraging the UNL to expedite deep freeze amendment activation
* or deploy hotfixes via new amendments. In case of a new amendment, we'd
* only have to change this line setting 'enforce' variable.
* enforce = view.rules().enabled(featureDeepFreeze) ||
* view.rules().enabled(fixFreezeExploit);
*/
[[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
for (auto const& [issue, changes] : balanceChanges_)
{
auto const issuerSle = findIssuer(issue.account, view);
// It should be impossible for the issuer to not be found, but check
// just in case so rippled doesn't crash in release.
if (!issuerSle)
{
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT(
enforce,
"xrpl::TransfersNotFrozen::finalize : enforce "
"invariant.");
if (enforce)
{
return false;
}
continue;
}
if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce))
{
return false;
}
}
return true;
}
bool
TransfersNotFrozen::isValidEntry(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
// `after` can never be null, even if the trust line is deleted.
XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
if (!after)
{
return false;
}
if (after->getType() == ltACCOUNT_ROOT)
{
possibleIssuers_.emplace(after->at(sfAccount), after);
return false;
}
/* While LedgerEntryTypesMatch invariant also checks types, all invariants
* are processed regardless of previous failures.
*
* This type check is still necessary here because it prevents potential
* issues in subsequent processing.
*/
return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE);
}
STAmount
TransfersNotFrozen::calculateBalanceChange(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after,
bool isDelete)
{
auto const getBalance = [](auto const& line, auto const& other, bool zero) {
STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
return zero ? amt.zeroed() : amt;
};
/* Trust lines can be created dynamically by other transactions such as
* Payment and OfferCreate that cross offers. Such trust line won't be
* created frozen, but the sender might be, so the starting balance must be
* treated as zero.
*/
auto const balanceBefore = getBalance(before, after, false);
/* Same as above, trust lines can be dynamically deleted, and for frozen
* trust lines, payments not involving the issuer must be blocked. This is
* achieved by treating the final balance as zero when isDelete=true to
* ensure frozen line restrictions are enforced even during deletion.
*/
auto const balanceAfter = getBalance(after, before, isDelete);
return balanceAfter - balanceBefore;
}
void
TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change)
{
XRPL_ASSERT(
change.balanceChangeSign,
"xrpl::TransfersNotFrozen::recordBalance : valid trustline "
"balance sign.");
auto& changes = balanceChanges_[issue];
if (change.balanceChangeSign < 0)
changes.senders.emplace_back(std::move(change));
else
changes.receivers.emplace_back(std::move(change));
}
void
TransfersNotFrozen::recordBalanceChanges(
std::shared_ptr<SLE const> const& after,
STAmount const& balanceChange)
{
auto const balanceChangeSign = balanceChange.signum();
auto const currency = after->at(sfBalance).getCurrency();
// Change from low account's perspective, which is trust line default
recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign});
// Change from high account's perspective, which reverses the sign.
recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign});
}
std::shared_ptr<SLE const>
TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view)
{
if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
{
return it->second;
}
return view.read(keylet::account(issuerID));
}
bool
TransfersNotFrozen::validateIssuerChanges(
std::shared_ptr<SLE const> const& issuer,
IssuerChanges const& changes,
STTx const& tx,
beast::Journal const& j,
bool enforce)
{
if (!issuer)
{
return false;
}
bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
if (changes.receivers.empty() || changes.senders.empty())
{
/* If there are no receivers, then the holder(s) are returning
* their tokens to the issuer. Likewise, if there are no
* senders, then the issuer is issuing tokens to the holder(s).
* This is allowed regardless of the issuer's freeze flags. (The
* holder may have contradicting freeze flags, but that will be
* checked when the holder is treated as issuer.)
*/
return true;
}
for (auto const& actors : {changes.senders, changes.receivers})
{
for (auto const& change : actors)
{
bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze))
{
return false;
}
}
}
return true;
}
bool
TransfersNotFrozen::validateFrozenState(
BalanceChange const& change,
bool high,
STTx const& tx,
beast::Journal const& j,
bool enforce,
bool globalFreeze)
{
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))
{
JLOG(j.debug()) << "Invariant check allowing funds to be moved "
<< (change.balanceChangeSign > 0 ? "to" : "from")
<< " a frozen trustline for AMMClawback " << tx.getTransactionID();
return true;
}
JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
<< tx.getTransactionID();
// The comment above starting with "assert(enforce)" explains this assert.
XRPL_ASSERT(
enforce,
"xrpl::TransfersNotFrozen::validateFrozenState : enforce "
"invariant.");
if (enforce)
{
return false;
}
return true;
}
} // namespace xrpl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
#include <xrpl/tx/invariants/LoanInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFormats.h>
namespace xrpl {
void
ValidLoanBroker::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after)
{
if (after->getType() == ltLOAN_BROKER)
{
auto& broker = brokers_[after->key()];
broker.brokerBefore = before;
broker.brokerAfter = after;
}
else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = after->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
brokers_.emplace(loanBrokerID, BrokerInfo{});
}
else if (after->getType() == ltRIPPLE_STATE)
{
lines_.emplace_back(after);
}
else if (after->getType() == ltMPTOKEN)
{
mpts_.emplace_back(after);
}
}
}
bool
ValidLoanBroker::goodZeroDirectory(
ReadView const& view,
SLE::const_ref dir,
beast::Journal const& j) const
{
auto const next = dir->at(~sfIndexNext);
auto const prev = dir->at(~sfIndexPrevious);
if ((prev && *prev) || (next && *next))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple directory pages";
return false;
}
auto indexes = dir->getFieldV256(sfIndexes);
if (indexes.size() > 1)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple indexes in the Directory root";
return false;
}
if (indexes.size() == 1)
{
auto const index = indexes.value().front();
auto const sle = view.read(keylet::unchecked(index));
if (!sle)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt";
return false;
}
if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
"OwnerCount has an unexpected entry in the directory";
return false;
}
}
return true;
}
bool
ValidLoanBroker::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Loan Brokers will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& line : lines_)
{
for (auto const& field : {&sfLowLimit, &sfHighLimit})
{
auto const account = view.read(keylet::account(line->at(*field).getIssuer()));
// This Invariant doesn't know about the rules for Trust Lines, so
// if the account is missing, don't treat it as an error. This
// loop is only concerned with finding Broker pseudo-accounts
if (account && account->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = account->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
brokers_.emplace(loanBrokerID, BrokerInfo{});
}
}
}
for (auto const& mpt : mpts_)
{
auto const account = view.read(keylet::account(mpt->at(sfAccount)));
// This Invariant doesn't know about the rules for MPTokens, so
// if the account is missing, don't treat is as an error. This
// loop is only concerned with finding Broker pseudo-accounts
if (account && account->isFieldPresent(sfLoanBrokerID))
{
auto const& loanBrokerID = account->at(sfLoanBrokerID);
// create an entry if one doesn't already exist
brokers_.emplace(loanBrokerID, BrokerInfo{});
}
}
for (auto const& [brokerID, broker] : brokers_)
{
auto const& after =
broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID));
if (!after)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
return false;
}
auto const& before = broker.brokerBefore;
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
// one node (the root), which will only hold entries for `RippleState`
// or `MPToken` objects.
if (after->at(sfOwnerCount) == 0)
{
auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
if (dir)
{
if (!goodZeroDirectory(view, dir, j))
{
return false;
}
}
}
if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
"decreased";
return false;
}
if (after->at(sfDebtTotal) < 0)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative";
return false;
}
if (after->at(sfCoverAvailable) < 0)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative";
return false;
}
auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
if (!vault)
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid";
return false;
}
auto const& vaultAsset = vault->at(sfAsset);
if (after->at(sfCoverAvailable) < accountHolds(
view,
after->at(sfAccount),
vaultAsset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
"is less than pseudo-account asset balance";
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
ValidLoan::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN)
{
loans_.emplace_back(before, after);
}
}
bool
ValidLoan::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Loans will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : loans_)
{
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
// If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
if (after->at(sfPaymentRemaining) == 0 &&
(after->at(sfTotalValueOutstanding) != beast::zero ||
after->at(sfPrincipalOutstanding) != beast::zero ||
after->at(sfManagementFeeOutstanding) != beast::zero))
{
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
"remaining has not been paid off";
return false;
}
// If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid
// off
if (after->at(sfPaymentRemaining) != 0 &&
after->at(sfTotalValueOutstanding) == beast::zero &&
after->at(sfPrincipalOutstanding) == beast::zero &&
after->at(sfManagementFeeOutstanding) == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
"remaining has not been paid off";
return false;
}
if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
{
JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
return false;
}
// Must not be negative - STNumber
for (auto const field :
{&sfLoanServiceFee,
&sfLatePaymentFee,
&sfClosePaymentFee,
&sfPrincipalOutstanding,
&sfTotalValueOutstanding,
&sfManagementFeeOutstanding})
{
if (after->at(*field) < 0)
{
JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative ";
return false;
}
}
// Must be positive - STNumber
for (auto const field : {
&sfPeriodicPayment,
})
{
if (after->at(*field) <= 0)
{
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
<< " is zero or negative ";
return false;
}
}
}
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,192 @@
#include <xrpl/tx/invariants/MPTInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
namespace xrpl {
void
ValidMPTIssuance::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
{
if (isDelete)
mptIssuancesDeleted_++;
else if (!before)
mptIssuancesCreated_++;
}
if (after && after->getType() == ltMPTOKEN)
{
if (isDelete)
mptokensDeleted_++;
else if (!before)
{
mptokensCreated_++;
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
if (mptIssue.getIssuer() == after->at(sfAccount))
mptCreatedByIssuer_ = true;
}
}
}
bool
ValidMPTIssuance::finalize(
STTx const& tx,
TER const result,
XRPAmount const _fee,
ReadView const& view,
beast::Journal const& j)
{
if (result == tesSUCCESS)
{
auto const& rules = view.rules();
[[maybe_unused]]
bool enforceCreatedByIssuer =
rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol);
if (mptCreatedByIssuer_)
{
JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer";
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT_PARTS(
enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken");
if (enforceCreatedByIssuer)
return false;
}
auto const txnType = tx.getTxnType();
if (hasPrivilege(tx, createMPTIssuance))
{
if (mptIssuancesCreated_ == 0)
{
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded without creating a MPT issuance";
}
else if (mptIssuancesDeleted_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded while removing MPT issuances";
}
else if (mptIssuancesCreated_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: transaction "
"succeeded but created multiple issuances";
}
return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
}
if (hasPrivilege(tx, destroyMPTIssuance))
{
if (mptIssuancesDeleted_ == 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
"succeeded without removing a MPT issuance";
}
else if (mptIssuancesCreated_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
"succeeded while creating MPT issuances";
}
else if (mptIssuancesDeleted_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
"succeeded but deleted multiple issuances";
}
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
}
bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol);
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
// non-amendment-gated side effects.
bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
(view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled);
if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish)
{
bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
if (mptIssuancesCreated_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but created MPT issuances";
return false;
}
else if (mptIssuancesDeleted_ > 0)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
"succeeded but deleted issuances";
return false;
}
else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
"succeeded but created/deleted mptokens";
return false;
}
else if (
!submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) &&
(mptokensCreated_ + mptokensDeleted_ != 1))
{
// if the holder submitted this tx, then a mptoken must be
// either created or deleted.
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
"succeeded but created/deleted bad number of mptokens";
return false;
}
return true;
}
if (txnType == ttESCROW_FINISH)
{
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
// non-amendment-gated side effects.
XRPL_ASSERT_PARTS(
!enforceEscrowFinish, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx");
return true;
}
if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
return true;
}
if (mptIssuancesCreated_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created";
}
else if (mptIssuancesDeleted_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted";
}
else if (mptokensCreated_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: a MPToken was created";
}
else if (mptokensDeleted_ != 0)
{
JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted";
}
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 &&
mptokensDeleted_ == 0;
}
} // namespace xrpl

View File

@@ -0,0 +1,274 @@
#include <xrpl/tx/invariants/NFTInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/nftPageMask.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/transactors/NFT/NFTokenUtils.h>
namespace xrpl {
void
ValidNFTokenPage::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
static constexpr uint256 const& pageBits = nft::pageMask;
static constexpr uint256 const accountBits = ~pageBits;
if ((before && before->getType() != ltNFTOKEN_PAGE) ||
(after && after->getType() != ltNFTOKEN_PAGE))
return;
auto check = [this, isDelete](std::shared_ptr<SLE const> const& sle) {
uint256 const account = sle->key() & accountBits;
uint256 const hiLimit = sle->key() & pageBits;
std::optional<uint256> const prev = (*sle)[~sfPreviousPageMin];
// Make sure that any page links...
// 1. Are properly associated with the owning account and
// 2. The page is correctly ordered between links.
if (prev)
{
if (account != (*prev & accountBits))
badLink_ = true;
if (hiLimit <= (*prev & pageBits))
badLink_ = true;
}
if (auto const next = (*sle)[~sfNextPageMin])
{
if (account != (*next & accountBits))
badLink_ = true;
if (hiLimit >= (*next & pageBits))
badLink_ = true;
}
{
auto const& nftokens = sle->getFieldArray(sfNFTokens);
// An NFTokenPage should never contain too many tokens or be empty.
if (std::size_t const nftokenCount = nftokens.size();
(!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage)
invalidSize_ = true;
// If prev is valid, use it to establish a lower bound for
// page entries. If prev is not valid the lower bound is zero.
uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero);
// Also verify that all NFTokenIDs in the page are sorted.
uint256 loCmp = loLimit;
for (auto const& obj : nftokens)
{
uint256 const tokenID = obj[sfNFTokenID];
if (!nft::compareTokens(loCmp, tokenID))
badSort_ = true;
loCmp = tokenID;
// None of the NFTs on this page should belong on lower or
// higher pages.
if (uint256 const tokenPageBits = tokenID & pageBits;
tokenPageBits < loLimit || tokenPageBits >= hiLimit)
badEntry_ = true;
if (auto uri = obj[~sfURI]; uri && uri->empty())
badURI_ = true;
}
}
};
if (before)
{
check(before);
// While an account's NFToken directory contains any NFTokens, the last
// NFTokenPage (with 96 bits of 1 in the low part of the index) should
// never be deleted.
if (isDelete && (before->key() & nft::pageMask) == nft::pageMask &&
before->isFieldPresent(sfPreviousPageMin))
{
deletedFinalPage_ = true;
}
}
if (after)
check(after);
if (!isDelete && before && after)
{
// If the NFTokenPage
// 1. Has a NextMinPage field in before, but loses it in after, and
// 2. This is not the last page in the directory
// Then we have identified a corruption in the links between the
// NFToken pages in the NFToken directory.
if ((before->key() & nft::pageMask) != nft::pageMask &&
before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin))
{
deletedLink_ = true;
}
}
}
bool
ValidNFTokenPage::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (badLink_)
{
JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked.";
return false;
}
if (badEntry_)
{
JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page.";
return false;
}
if (badSort_)
{
JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted.";
return false;
}
if (badURI_)
{
JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI.";
return false;
}
if (invalidSize_)
{
JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size.";
return false;
}
if (view.rules().enabled(fixNFTokenPageLinks))
{
if (deletedFinalPage_)
{
JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with "
"non-empty directory.";
return false;
}
if (deletedLink_)
{
JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link.";
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
NFTokenCountTracking::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (before && before->getType() == ltACCOUNT_ROOT)
{
beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0);
beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0);
}
if (after && after->getType() == ltACCOUNT_ROOT)
{
afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0);
afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0);
}
}
bool
NFTokenCountTracking::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (!hasPrivilege(tx, changeNFTCounts))
{
if (beforeMintedTotal != afterMintedTotal)
{
JLOG(j.fatal()) << "Invariant failed: the number of minted tokens "
"changed without a mint transaction!";
return false;
}
if (beforeBurnedTotal != afterBurnedTotal)
{
JLOG(j.fatal()) << "Invariant failed: the number of burned tokens "
"changed without a burn transaction!";
return false;
}
return true;
}
if (tx.getTxnType() == ttNFTOKEN_MINT)
{
if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal)
{
JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase "
"the number of minted tokens.";
return false;
}
if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal)
{
JLOG(j.fatal()) << "Invariant failed: failed minting changed the "
"number of minted tokens.";
return false;
}
if (beforeBurnedTotal != afterBurnedTotal)
{
JLOG(j.fatal()) << "Invariant failed: minting changed the number of "
"burned tokens.";
return false;
}
}
if (tx.getTxnType() == ttNFTOKEN_BURN)
{
if (result == tesSUCCESS)
{
if (beforeBurnedTotal >= afterBurnedTotal)
{
JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase "
"the number of burned tokens.";
return false;
}
}
if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal)
{
JLOG(j.fatal()) << "Invariant failed: failed burning changed the "
"number of burned tokens.";
return false;
}
if (beforeMintedTotal != afterMintedTotal)
{
JLOG(j.fatal()) << "Invariant failed: burning changed the number of "
"minted tokens.";
return false;
}
}
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,93 @@
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/TxFormats.h>
namespace xrpl {
void
ValidPermissionedDEX::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltDIR_NODE)
{
if (after->isFieldPresent(sfDomainID))
domains_.insert(after->getFieldH256(sfDomainID));
}
if (after && after->getType() == ltOFFER)
{
if (after->isFieldPresent(sfDomainID))
domains_.insert(after->getFieldH256(sfDomainID));
else
regularOffers_ = true;
// if a hybrid offer is missing domain or additional book, there's
// something wrong
if (after->isFlag(lsfHybrid) &&
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
after->getFieldArray(sfAdditionalBooks).size() > 1))
badHybrids_ = true;
}
}
bool
ValidPermissionedDEX::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
auto const txType = tx.getTxnType();
if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS)
return true;
// For each offercreate transaction, check if
// permissioned offers are valid
if (txType == ttOFFER_CREATE && badHybrids_)
{
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
return false;
}
if (!tx.isFieldPresent(sfDomainID))
return true;
auto const domain = tx.getFieldH256(sfDomainID);
if (!view.exists(keylet::permissionedDomain(domain)))
{
JLOG(j.fatal()) << "Invariant failed: domain doesn't exist";
return false;
}
// for both payment and offercreate, there shouldn't be another domain
// that's different from the domain specified
for (auto const& d : domains_)
{
if (d != domain)
{
JLOG(j.fatal()) << "Invariant failed: transaction"
" consumed wrong domains";
return false;
}
}
if (regularOffers_)
{
JLOG(j.fatal()) << "Invariant failed: domain transaction"
" affected regular offers";
return false;
}
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,162 @@
#include <xrpl/tx/invariants/PermissionedDomainInvariant.h>
//
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/TxFormats.h>
namespace xrpl {
void
ValidPermissionedDomain::visitEntry(
bool isDel,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
return;
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
return;
auto check = [isDel](std::vector<SleStatus>& sleStatus, std::shared_ptr<SLE const> const& sle) {
auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);
auto const sorted = credentials::makeSorted(credentials);
SleStatus ss{credentials.size(), false, !sorted.empty(), isDel};
// If array have duplicates then all the other checks are invalid
if (ss.isUnique_)
{
unsigned i = 0;
for (auto const& cred : sorted)
{
auto const& credTx = credentials[i++];
ss.isSorted_ =
(cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]);
if (!ss.isSorted_)
break;
}
}
sleStatus.emplace_back(std::move(ss));
};
if (after)
check(sleStatus_, after);
}
bool
ValidPermissionedDomain::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
auto check = [](SleStatus const& sleStatus, beast::Journal const& j) {
if (!sleStatus.credentialsSize_)
{
JLOG(j.fatal()) << "Invariant failed: permissioned domain with "
"no rules.";
return false;
}
if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize)
{
JLOG(j.fatal()) << "Invariant failed: permissioned domain bad "
"credentials size "
<< sleStatus.credentialsSize_;
return false;
}
if (!sleStatus.isUnique_)
{
JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
"aren't unique";
return false;
}
if (!sleStatus.isSorted_)
{
JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
"aren't sorted";
return false;
}
return true;
};
if (view.rules().enabled(fixPermissionedDomainInvariant))
{
// No permissioned domains should be affected if the transaction failed
if (result != tesSUCCESS)
// If nothing changed, all is good. If there were changes, that's
// bad.
return sleStatus_.empty();
if (sleStatus_.size() > 1)
{
JLOG(j.fatal()) << "Invariant failed: transaction affected more "
"than 1 permissioned domain entry.";
return false;
}
switch (tx.getTxnType())
{
case ttPERMISSIONED_DOMAIN_SET: {
if (sleStatus_.empty())
{
JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
"PermissionedDomainSet";
return false;
}
auto const& sleStatus = sleStatus_[0];
if (sleStatus.isDelete_)
{
JLOG(j.fatal()) << "Invariant failed: domain object "
"deleted by PermissionedDomainSet";
return false;
}
return check(sleStatus, j);
}
case ttPERMISSIONED_DOMAIN_DELETE: {
if (sleStatus_.empty())
{
JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
"PermissionedDomainDelete";
return false;
}
if (!sleStatus_[0].isDelete_)
{
JLOG(j.fatal()) << "Invariant failed: domain object "
"modified, but not deleted by "
"PermissionedDomainDelete";
return false;
}
return true;
}
default: {
if (!sleStatus_.empty())
{
JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size()
<< " domain object(s) affected by an "
"unauthorized transaction. "
<< tx.getTxnType();
return false;
}
return true;
}
}
}
else
{
if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS ||
sleStatus_.empty())
return true;
return check(sleStatus_[0], j);
}
}
} // namespace xrpl

View File

@@ -0,0 +1,926 @@
#include <xrpl/tx/invariants/VaultInvariant.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/View.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/STNumber.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
namespace xrpl {
ValidVault::Vault
ValidVault::Vault::make(SLE const& from)
{
XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object");
ValidVault::Vault self;
self.key = from.key();
self.asset = from.at(sfAsset);
self.pseudoId = from.getAccountID(sfAccount);
self.owner = from.at(sfOwner);
self.shareMPTID = from.getFieldH192(sfShareMPTID);
self.assetsTotal = from.at(sfAssetsTotal);
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
return self;
}
ValidVault::Shares
ValidVault::Shares::make(SLE const& from)
{
XRPL_ASSERT(
from.getType() == ltMPTOKEN_ISSUANCE,
"ValidVault::Shares::make : from MPTokenIssuance object");
ValidVault::Shares self;
self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
self.sharesTotal = from.at(sfOutstandingAmount);
self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount);
return self;
}
void
ValidVault::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
// If `before` is empty, this means an object is being created, in which
// case `isDelete` must be false. Otherwise `before` and `after` are set and
// `isDelete` indicates whether an object is being deleted or modified.
XRPL_ASSERT(
after != nullptr && (before != nullptr || !isDelete),
"xrpl::ValidVault::visitEntry : some object is available");
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), so the
// invariants can validate that the change in account balances matches the
// change in vault balances, stored to deltas_ at the end of this function.
Number balanceDelta{};
std::int8_t sign = 0;
if (before)
{
switch (before->getType())
{
case ltVAULT:
beforeVault_.push_back(Vault::make(*before));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
sign = 1;
break;
case ltMPTOKEN:
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balanceDelta = before->getFieldAmount(sfBalance);
sign = -1;
break;
default:;
}
}
if (!isDelete && after)
{
switch (after->getType())
{
case ltVAULT:
afterVault_.push_back(Vault::make(*after));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balanceDelta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
sign = 1;
break;
case ltMPTOKEN:
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balanceDelta -= Number(after->getFieldAmount(sfBalance));
sign = -1;
break;
default:;
}
}
uint256 const key = (before ? before->key() : after->key());
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
deltas_[key] = balanceDelta * sign;
}
bool
ValidVault::finalize(
STTx const& tx,
TER const ret,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j)
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
if (!isTesSuccess(ret))
return true; // Do not perform checks
if (afterVault_.empty() && beforeVault_.empty())
{
if (hasPrivilege(tx, mustModifyVault))
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation succeeded without modifying "
"a vault";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant");
return !enforce;
}
return true; // Not a vault operation
}
else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault)))
{
JLOG(j.fatal()) << //
"Invariant failed: vault updated by a wrong transaction type";
XRPL_ASSERT(
enforce,
"xrpl::ValidVault::finalize : illegal vault transaction "
"invariant");
return !enforce; // Also not a vault operation
}
if (beforeVault_.size() > 1 || afterVault_.size() > 1)
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation updated more than single vault";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant");
return !enforce; // That's all we can do here
}
auto const txnType = tx.getTxnType();
// We do special handling for ttVAULT_DELETE first, because it's the only
// vault-modifying transaction without an "after" state of the vault
if (afterVault_.empty())
{
if (txnType != ttVAULT_DELETE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault deleted by a wrong transaction type";
XRPL_ASSERT(
enforce,
"xrpl::ValidVault::finalize : illegal vault deletion "
"invariant");
return !enforce; // That's all we can do here
}
// Note, if afterVault_ is empty then we know that beforeVault_ is not
// empty, as enforced at the top of this function
auto const& beforeVault = beforeVault_[0];
// At this moment we only know a vault is being deleted and there
// might be some MPTokenIssuance objects which are deleted in the
// same transaction. Find the one matching this vault.
auto const deletedShares = [&]() -> std::optional<Shares> {
for (auto const& e : beforeMPTs_)
{
if (e.share.getMptID() == beforeVault.shareMPTID)
return std::move(e);
}
return std::nullopt;
}();
if (!deletedShares)
{
JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
"delete shares";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant");
return !enforce; // That's all we can do here
}
bool result = true;
if (deletedShares->sharesTotal != 0)
{
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
"shares outstanding";
result = false;
}
if (beforeVault.assetsTotal != zero)
{
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
"assets outstanding";
result = false;
}
if (beforeVault.assetsAvailable != zero)
{
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
"assets available";
result = false;
}
return result;
}
else if (txnType == ttVAULT_DELETE)
{
JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
"deleting a vault";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant");
return !enforce; // That's all we can do here
}
// Note, `afterVault_.empty()` is handled above
auto const& afterVault = afterVault_[0];
XRPL_ASSERT(
beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
"xrpl::ValidVault::finalize : single vault operation");
auto const updatedShares = [&]() -> std::optional<Shares> {
// At this moment we only know that a vault is being updated and there
// might be some MPTokenIssuance objects which are also updated in the
// same transaction. Find the one matching the shares to this vault.
// Note, we expect updatedMPTs collection to be extremely small. For
// such collections linear search is faster than lookup.
for (auto const& e : afterMPTs_)
{
if (e.share.getMptID() == afterVault.shareMPTID)
return e;
}
auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID));
return sleShares ? std::optional<Shares>(Shares::make(*sleShares)) : std::nullopt;
}();
bool result = true;
// Universal transaction checks
if (!beforeVault_.empty())
{
auto const& beforeVault = beforeVault_[0];
if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
afterVault.shareMPTID != beforeVault.shareMPTID)
{
JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
result = false;
}
}
if (!updatedShares)
{
JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant");
return !enforce; // That's all we can do here
}
if (updatedShares->sharesTotal == 0)
{
if (afterVault.assetsTotal != zero)
{
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
"vault must have no assets outstanding";
result = false;
}
if (afterVault.assetsAvailable != zero)
{
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
"vault must have no assets available";
result = false;
}
}
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
{
JLOG(j.fatal()) //
<< "Invariant failed: updated shares must not exceed maximum "
<< updatedShares->sharesMaximum;
result = false;
}
if (afterVault.assetsAvailable < zero)
{
JLOG(j.fatal()) << "Invariant failed: assets available must be positive";
result = false;
}
if (afterVault.assetsAvailable > afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: assets available must "
"not be greater than assets outstanding";
result = false;
}
else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable)
{
JLOG(j.fatal()) //
<< "Invariant failed: loss unrealized must not exceed "
"the difference between assets outstanding and available";
result = false;
}
if (afterVault.assetsTotal < zero)
{
JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive";
result = false;
}
if (afterVault.assetsMaximum < zero)
{
JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive";
result = false;
}
// Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
// enforcing invariants on transaction types other than ttVAULT_CREATE
if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault created by a wrong transaction type";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant");
return !enforce; // That's all we can do here
}
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change loss "
"unrealized";
result = false;
}
auto const beforeShares = [&]() -> std::optional<Shares> {
if (beforeVault_.empty())
return std::nullopt;
auto const& beforeVault = beforeVault_[0];
for (auto const& e : beforeMPTs_)
{
if (e.share.getMptID() == beforeVault.shareMPTID)
return std::move(e);
}
return std::nullopt;
}();
if (!beforeShares &&
(tx.getTxnType() == ttVAULT_DEPOSIT || //
tx.getTxnType() == ttVAULT_WITHDRAW || //
tx.getTxnType() == ttVAULT_CLAWBACK))
{
JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
"without updating shares";
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant");
return !enforce; // That's all we can do here
}
auto const& vaultAsset = afterVault.asset;
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
auto const get = //
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
if (it == deltas_.end())
return std::nullopt;
return it->second * sign;
};
return std::visit(
[&]<typename TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
if (isXRP(issue))
return get(deltas_.find(keylet::account(id).key));
return get(
deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1);
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
{
return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key));
}
},
vaultAsset.value());
};
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Delegated transaction; no need to compensate for fees
if (auto const delegate = tx[~sfDelegate];
delegate.has_value() && *delegate != tx[sfAccount])
return ret;
*ret += fee.drops();
if (*ret == zero)
return std::nullopt;
return ret;
};
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
auto const it = [&]() {
if (id == afterVault.pseudoId)
return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key);
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<Number>(it->second) : std::nullopt;
};
auto const vaultHoldsNoAssets = [&](Vault const& vault) {
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
};
// Technically this does not need to be a lambda, but it's more
// convenient thanks to early "return false"; the not-so-nice
// alternatives are several layers of nested if/else or more complex
// (i.e. brittle) if statements.
result &= [&]() {
switch (txnType)
{
case ttVAULT_CREATE: {
bool result = true;
if (!beforeVault_.empty())
{
JLOG(j.fatal()) //
<< "Invariant failed: create operation must not have "
"updated a vault";
result = false;
}
if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero ||
afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0)
{
JLOG(j.fatal()) //
<< "Invariant failed: created vault must be empty";
result = false;
}
if (afterVault.pseudoId != updatedShares->share.getIssuer())
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer and vault "
"pseudo-account must be the same";
result = false;
}
auto const sleSharesIssuer =
view.read(keylet::account(updatedShares->share.getIssuer()));
if (!sleSharesIssuer)
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer must exist";
return false;
}
if (!isPseudoAccount(sleSharesIssuer))
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer must be a "
"pseudo-account";
result = false;
}
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
!vaultId || *vaultId != afterVault.key)
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer pseudo-account "
"must point back to the vault";
result = false;
}
return result;
}
case ttVAULT_SET: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: set must not change vault balance";
result = false;
}
if (beforeVault.assetsTotal != afterVault.assetsTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: set must not change assets "
"outstanding";
result = false;
}
if (afterVault.assetsMaximum > zero &&
afterVault.assetsTotal > afterVault.assetsMaximum)
{
JLOG(j.fatal()) << //
"Invariant failed: set assets outstanding must not "
"exceed assets maximum";
result = false;
}
if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << //
"Invariant failed: set must not change assets "
"available";
result = false;
}
if (beforeShares && updatedShares &&
beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: set must not change shares "
"outstanding";
result = false;
}
return result;
}
case ttVAULT_DEPOSIT: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault balance";
return false; // That's all we can do
}
if (*vaultDeltaAssets > tx[sfAmount])
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault "
"balance by more than deposited amount";
result = false;
}
if (*vaultDeltaAssets <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
result = false;
}
// Any payments (including deposits) made by the issuer
// do not change their balance, but create funds instead.
bool const issuerDeposit = [&]() -> bool {
if (vaultAsset.native())
return false;
return tx[sfAccount] == vaultAsset.getIssuer();
}();
if (!issuerDeposit)
{
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"balance";
return false;
}
if (*accountDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must decrease depositor "
"balance";
result = false;
}
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and "
"depositor balance by equal amount";
result = false;
}
}
if (afterVault.assetsMaximum > zero &&
afterVault.assetsTotal > afterVault.assetsMaximum)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit assets outstanding must not "
"exceed assets maximum";
result = false;
}
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"shares";
return false; // That's all we can do
}
if (*accountDeltaShares <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase depositor "
"shares";
result = false;
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and "
"vault shares by equal amount";
result = false;
}
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"outstanding must add up";
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"available must add up";
result = false;
}
return result;
}
case ttVAULT_WITHDRAW: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(),
"xrpl::ValidVault::finalize : withdrawal updated a "
"vault");
auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"change vault balance";
return false; // That's all we can do
}
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"decrease vault balance";
result = false;
}
// Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool {
if (vaultAsset.native())
return false;
auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
return destination == vaultAsset.getIssuer();
}();
if (!issuerWithdrawal)
{
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta = [&]() -> std::optional<Number> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
return deltaAssets(*destination);
return std::nullopt;
}();
if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one "
"destination balance";
return false;
}
auto const destinationDelta = //
accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
if (destinationDelta <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must increase "
"destination balance";
result = false;
}
if (*vaultDeltaAssets * -1 != destinationDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault "
"and destination balance by equal amount";
result = false;
}
}
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor "
"shares";
return false;
}
if (*accountDeltaShares >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must decrease depositor "
"shares";
result = false;
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor "
"and vault shares by equal amount";
result = false;
}
// Note, vaultBalance is negative (see check above)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets outstanding must add up";
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets available must add up";
result = false;
}
return result;
}
case ttVAULT_CLAWBACK: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault");
auto const& beforeVault = beforeVault_[0];
if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
{
// The owner can use clawback to force-burn shares when the
// vault is empty but there are outstanding shares
if (!(beforeShares && beforeShares->sharesTotal > 0 &&
vaultHoldsNoAssets(beforeVault) && beforeVault.owner == tx[sfAccount]))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback may only be performed "
"by the asset issuer, or by the vault owner of an "
"empty vault";
return false; // That's all we can do
}
}
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
{
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault "
"balance";
result = false;
}
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding "
"must add up";
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available "
"must add up";
result = false;
}
}
else if (!vaultHoldsNoAssets(beforeVault))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault balance";
return false; // That's all we can do
}
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder shares";
return false; // That's all we can do
}
if (*accountDeltaShares >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease holder "
"shares";
result = false;
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and "
"vault shares by equal amount";
result = false;
}
return result;
}
case ttLOAN_SET:
case ttLOAN_MANAGE:
case ttLOAN_PAY: {
// TBD
return true;
}
default:
// LCOV_EXCL_START
UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
return false;
// LCOV_EXCL_STOP
}
}();
if (!result)
{
// The comment at the top of this file starting with "assert(enforce)"
// explains this assert.
XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants");
return !enforce;
}
return true;
}
} // namespace xrpl

View File

@@ -107,18 +107,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
if (signer.isFieldPresent(sfTxnSignature))
signerCount += 1;
else if (signer.isFieldPresent(sfSigners))
{
auto const& nestedSigners = signer.getFieldArray(sfSigners);
// LCOV_EXCL_START
if (nestedSigners.size() > STTx::maxMultiSigners)
{
JLOG(debugLog().error())
<< "BatchTrace: Nested Signers array exceeds max entries.";
return XRPAmount{INITIAL_XRP};
}
// LCOV_EXCL_STOP
signerCount += nestedSigners.size();
}
signerCount += signer.getFieldArray(sfSigners).size();
}
}
@@ -216,14 +205,6 @@ Batch::preflight(PreflightContext const& ctx)
return temARRAY_TOO_LARGE;
}
if (ctx.tx.isFieldPresent(sfBatchSigners) &&
ctx.tx.getFieldArray(sfBatchSigners).size() > maxBatchTxCount)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
<< "signers array exceeds 8 entries.";
return temARRAY_TOO_LARGE;
}
// Validation Inner Batch Txns
std::unordered_set<uint256> uniqueHashes;
std::unordered_map<AccountID, std::unordered_set<std::uint32_t>> accountSeqTicket;
@@ -445,7 +426,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
if (requiredSigners.erase(signerAccount) == 0)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "extra signer provided: " << signerAccount;
<< "no account signature for inner txn.";
return temBAD_SIGNER;
}
}
@@ -470,54 +451,6 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
Batch::checkBatchSign(PreclaimContext const& ctx)
{
NotTEC ret = tesSUCCESS;
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
auto const idAccount = signer.getAccountID(sfAccount);
Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
// LCOV_EXCL_START
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH;
// LCOV_EXCL_STOP
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
if (sleAccount)
{
if (isPseudoAccount(sleAccount))
return tefBAD_AUTH;
if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
if (idAccount != idSigner)
return tefBAD_AUTH;
// A batch can include transactions from an un-created account ONLY
// when the account master key is the signer
}
}
}
return ret;
}
/**
* @brief Checks the validity of signatures for a batch transaction.
*
@@ -526,7 +459,7 @@ Batch::checkBatchSign(PreclaimContext const& ctx)
* corresponding error code.
*
* Next, it verifies the batch-specific signature requirements by calling
* Batch::checkBatchSign. If this check fails, it also returns the
* Transactor::checkBatchSign. If this check fails, it also returns the
* corresponding error code.
*
* If both checks succeed, the function returns tesSUCCESS.
@@ -541,11 +474,8 @@ Batch::checkSign(PreclaimContext const& ctx)
if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.isFieldPresent(sfBatchSigners))
{
if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret))
return ret;
}
if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret))
return ret;
return tesSUCCESS;
}

View File

@@ -26,7 +26,7 @@ LoanSet::preflight(PreflightContext const& ctx)
auto const& tx = ctx.tx;
// Special case for Batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) &&
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
!tx.isFieldPresent(sfCounterparty))
{
auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});

View File

@@ -1,10 +1,9 @@
#include <xrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.h>
//
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.h>
#include <optional>
namespace xrpl {

View File

@@ -141,11 +141,14 @@ class Batch_test : public beast::unit_test::suite
using namespace test::jtx;
using namespace std::literals;
bool const withInnerSigFix = features[fixBatchInnerSigs];
for (bool const withBatch : {true, false})
{
testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled")
<< ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled");
auto const amend = withBatch ? features : features - featureBatchV1_1;
auto const amend = withBatch ? features : features - featureBatch;
test::jtx::Env env{*this, amend};
@@ -550,7 +553,6 @@ class Batch_test : public beast::unit_test::suite
Serializer msg;
serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs());
finishMultiSigningData(bob.id(), msg);
auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
bob.human();
@@ -1403,7 +1405,7 @@ class Batch_test : public beast::unit_test::suite
env.close();
}
// temARRAY_TOO_LARGE: Batch preflight: signers array exceeds 8 entries.
// temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries.
{
test::jtx::Env env{*this, features};
@@ -2189,16 +2191,22 @@ class Batch_test : public beast::unit_test::suite
void
doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
{
std::string const testName =
std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
bool const withInnerSigFix = features[fixBatchInnerSigs];
auto const amend = withBatch ? features : features - featureBatchV1_1;
std::string const testName = [&]() {
std::stringstream ss;
ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled")
<< ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": ";
return ss.str();
}();
auto const amend = withBatch ? features : features - featureBatch;
using namespace test::jtx;
using namespace std::literals;
test::jtx::Env env{*this, amend};
if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
if (!BEAST_EXPECT(amend[featureBatch] == withBatch))
return;
auto const alice = Account("alice");
@@ -2320,7 +2328,8 @@ class Batch_test : public beast::unit_test::suite
s.slice(),
__LINE__,
"fails local checks: Empty SigningPubKey.",
"fails local checks: Empty SigningPubKey.");
"fails local checks: Empty SigningPubKey.",
withBatch && !withInnerSigFix);
}
// Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
@@ -2331,7 +2340,7 @@ class Batch_test : public beast::unit_test::suite
{
STTx amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, featureBatchV1_1);
obj.setFieldH256(sfAmendment, fixBatchInnerSigs);
obj.setFieldU32(sfLedgerSequence, seq);
obj.setFieldU32(sfFlags, tfInnerBatchTxn);
});
@@ -2343,7 +2352,8 @@ class Batch_test : public beast::unit_test::suite
"Pseudo-transaction",
s.slice(),
__LINE__,
"fails local checks: Empty SigningPubKey.",
withInnerSigFix ? "fails local checks: Empty SigningPubKey."
: "fails local checks: Cannot submit pseudo transactions.",
"fails local checks: Empty SigningPubKey.");
}
}
@@ -2404,53 +2414,6 @@ class Batch_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(bob) == XRP(1000));
}
void
testCheckAllSignatures(FeatureBitset features)
{
testcase("check all signatures");
using namespace test::jtx;
using namespace std::literals;
// Verifies that checkBatchSign validates all signers even when an
// unfunded account (signed with its master key) appears first in the
// sorted signer list. A funded account with an invalid signature must
// still be rejected with tefBAD_AUTH.
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
// "aaa" sorts before other accounts alphabetically, ensuring the
// unfunded account is checked first in the sorted signer list
auto const unfunded = Account("aaa");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, carol);
env.close();
// Verify sort order: unfunded.id() < carol.id()
BEAST_EXPECT(unfunded.id() < carol.id());
auto const seq = env.seq(alice);
auto const ledSeq = env.current()->seq();
auto const batchFee = batch::calcBatchFee(env, 2, 3);
// The batch includes:
// 1. alice pays unfunded (to create unfunded's account)
// 2. unfunded does a noop (signed by unfunded's master key - valid)
// 3. carol pays alice (signed by alice's key - INVALID since alice is
// not carol's regular key)
//
// checkBatchSign must validate all signers regardless of order.
// This must fail with tefBAD_AUTH.
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, unfunded, XRP(100)), seq + 1),
batch::inner(noop(unfunded), ledSeq),
batch::inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
batch::sig(unfunded, Reg{carol, alice}),
ter(tefBAD_AUTH));
env.close();
}
void
testAccountSet(FeatureBitset features)
{
@@ -4368,7 +4331,6 @@ class Batch_test : public beast::unit_test::suite
testIndependent(features);
testInnerSubmitRPC(features);
testAccountActivation(features);
testCheckAllSignatures(features);
testAccountSet(features);
testAccountDelete(features);
testLoan(features);
@@ -4394,6 +4356,7 @@ public:
{
using namespace test::jtx;
auto const sa = testable_amendments();
testWithFeats(sa - fixBatchInnerSigs);
testWithFeats(sa);
}
};

View File

@@ -5340,20 +5340,20 @@ class Vault_test : public beast::unit_test::suite
env.close();
// 2. Mantissa larger than uint64 max
env.set_parse_failure_expected(true);
try
{
tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1
env(tx, THISLINE);
BEAST_EXPECT(false);
BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
}
catch (parse_error const& e)
{
using namespace std::string_literals;
BEAST_EXPECT(
e.what() ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid "
"data."s);
e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
}
env.set_parse_failure_expected(false);
}
}

View File

@@ -74,7 +74,6 @@ sig::operator()(Env& env, JTx& jt) const
Serializer msg;
serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs());
finishMultiSigningData(e.acct.id(), msg);
auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice());
jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()});
}

View File

@@ -122,20 +122,52 @@ class AccountTx_test : public beast::unit_test::suite
{
auto const& payment = j[jss::result][jss::transactions][1u];
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
if (apiVersion >= 3)
{
// In API v3, server-added lower-case fields must
// not be in tx_json, but must be at result level
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(!payment[jss::tx_json].isMember(jss::date)) &&
(!payment[jss::tx_json].isMember(jss::ledger_index)) &&
(!payment[jss::tx_json].isMember(jss::ctid)) &&
// date and ctid must be at the transaction
// object level (outside tx_json) in API v3
(payment.isMember(jss::date)) && (payment.isMember(jss::ctid)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
}
else
{
// In API v2, date and ledger_index are still in
// tx_json for backwards compatibility
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(payment[jss::tx_json].isMember(jss::date)) &&
(payment[jss::tx_json].isMember(jss::ledger_index)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
}
}
else
return false;

View File

@@ -116,7 +116,7 @@ class Feature_test : public beast::unit_test::suite
// or removed, swap out for any other feature.
BEAST_EXPECT(
featureToName(fixRemoveNFTokenAutoTrustLine) == "fixRemoveNFTokenAutoTrustLine");
BEAST_EXPECT(featureToName(featureBatchV1_1) == "BatchV1_1");
BEAST_EXPECT(featureToName(featureBatch) == "Batch");
BEAST_EXPECT(featureToName(featureDID) == "DID");
BEAST_EXPECT(featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields");
BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow");

View File

@@ -760,6 +760,25 @@ class Transaction_test : public beast::unit_test::suite
result[jss::result][jss::ledger_hash] ==
"B41882E20F0EC6228417D28B9AE0F33833645D35F6799DFB782AC97FC4BB51"
"D2");
auto const& tx_json = result[jss::result][jss::tx_json];
if (apiVersion >= 3)
{
// In API v3, server-added lower-case fields must not appear
// inside tx_json; they are at the result level.
BEAST_EXPECT(!tx_json.isMember(jss::date));
BEAST_EXPECT(!tx_json.isMember(jss::ledger_index));
BEAST_EXPECT(!tx_json.isMember(jss::ctid));
// date must be at result level in API v3
BEAST_EXPECT(result[jss::result].isMember(jss::date));
}
else
{
// In API v2, date and ledger_index are still included in
// tx_json for backwards compatibility.
BEAST_EXPECT(tx_json.isMember(jss::date));
BEAST_EXPECT(tx_json.isMember(jss::ledger_index));
}
}
for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++)

View File

@@ -1119,8 +1119,7 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
}
// Enforce Network bar for batch txn
if (iTrans->isFlag(tfInnerBatchTxn) &&
m_ledgerMaster.getValidatedRules().enabled(featureBatchV1_1))
if (iTrans->isFlag(tfInnerBatchTxn) && m_ledgerMaster.getValidatedRules().enabled(featureBatch))
{
JLOG(m_journal.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present.";
return;
@@ -1186,7 +1185,7 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
// under no circumstances will we ever accept an inner txn within a batch
// txn from the network.
auto const sttx = *transaction->getSTransaction();
if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatchV1_1))
if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch))
{
transaction->setStatus(INVALID);
transaction->setResult(temINVALID_FLAG);

View File

@@ -141,28 +141,30 @@ Transaction::getJson(JsonOptions options, bool binary) const
ret[jss::inLedger] = mLedgerIndex;
}
// TODO: disable_API_prior_V3 to disable output of both `date` and
// `ledger_index` elements (taking precedence over include_date)
ret[jss::ledger_index] = mLedgerIndex;
if (options & JsonOptions::include_date)
if (!(options & JsonOptions::disable_API_prior_V3))
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
ret[jss::ledger_index] = mLedgerIndex;
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (options & JsonOptions::include_date)
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
if (mTxnSeq && netID)
{
std::optional<std::string> const ctid = RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (mTxnSeq && netID)
{
std::optional<std::string> const ctid =
RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
}
}
}

View File

@@ -1291,7 +1291,7 @@ PeerImp::handleTransaction(
// Charge strongly for attempting to relay a txn with tfInnerBatchTxn
// LCOV_EXCL_START
/*
There is no need to check whether the featureBatchV1_1 amendment is
There is no need to check whether the featureBatch amendment is
enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is
@@ -2740,7 +2740,7 @@ PeerImp::checkTransaction(
// charge strongly for relaying batch txns
// LCOV_EXCL_START
/*
There is no need to check whether the featureBatchV1_1 amendment is
There is no need to check whether the featureBatch amendment is
enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
@@ -11,6 +12,7 @@
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ErrorCodes.h>
@@ -286,8 +288,10 @@ populateJsonResponse(
auto const json_tx = (context.apiVersion > 1 ? jss::tx_json : jss::tx);
if (context.apiVersion > 1)
{
jvObj[json_tx] = txn->getJson(
JsonOptions::include_date | JsonOptions::disable_API_prior_V2, false);
auto const opts = context.apiVersion >= 3
? JsonOptions::disable_API_prior_V2 | JsonOptions::disable_API_prior_V3
: JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
jvObj[json_tx] = txn->getJson(opts, false);
jvObj[jss::hash] = to_string(txn->getID());
jvObj[jss::ledger_index] = txn->getLedger();
jvObj[jss::ledger_hash] =
@@ -295,7 +299,20 @@ populateJsonResponse(
if (auto closeTime =
context.ledgerMaster.getCloseTimeBySeq(txn->getLedger()))
{
jvObj[jss::close_time_iso] = to_string_iso(*closeTime);
if (context.apiVersion >= 3)
jvObj[jss::date] = closeTime->time_since_epoch().count();
}
if (context.apiVersion >= 3 && txnMeta)
{
uint32_t const lgrSeq = txn->getLedger();
uint32_t const txnIdx = txnMeta->getIndex();
uint32_t const netID = context.app.getNetworkIDService().getNetworkID();
if (auto const ctid = RPC::encodeCTID(lgrSeq, txnIdx, netID))
jvObj[jss::ctid] = *ctid;
}
}
else
jvObj[json_tx] = txn->getJson(JsonOptions::include_date);

View File

@@ -189,8 +189,14 @@ populateJsonResponse(
auto const& sttx = result.txn->getSTransaction();
if (context.apiVersion > 1)
{
constexpr auto optionsJson =
// In API v2, include_date and disable_API_prior_V2 are used to
// include date/ledger_index/ctid in tx_json. In API v3+, those
// fields are excluded from tx_json and are only at result level.
constexpr auto optionsV2 =
JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
constexpr auto optionsV3 =
JsonOptions::disable_API_prior_V2 | JsonOptions::disable_API_prior_V3;
auto const optionsJson = context.apiVersion >= 3 ? optionsV3 : optionsV2;
if (args.binary)
response[jss::tx_blob] = result.txn->getJson(optionsJson, true);
else
@@ -210,7 +216,11 @@ populateJsonResponse(
{
response[jss::ledger_index] = result.txn->getLedger();
if (result.closeTime)
{
response[jss::close_time_iso] = to_string_iso(*result.closeTime);
if (context.apiVersion >= 3)
response[jss::date] = result.closeTime->time_since_epoch().count();
}
}
}
else